* Copyright 2005-2008, Cake Software Foundation, Inc. * 1785 E. Sahara Avenue, Suite 490-204 * Las Vegas, Nevada 89104 * * Licensed under The MIT License * Redistributions of files must retain the above copyright notice. * * @filesource * @copyright Copyright 2005-2008, Cake Software Foundation, Inc. * @link http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project * @package cake * @subpackage cake.cake.libs * @since CakePHP(tm) v 1.2.4560 * @version $Revision$ * @modifiedby $LastChangedBy$ * @lastmodified $Date$ * @license http://www.opensource.org/licenses/mit-license.php The MIT License */ /** * Included libraries. * */ if (!class_exists('Object')) { uses('object'); } if (!class_exists('CakeLog')) { uses('cake_log'); } /** * Provide custom logging and error handling. * * Debugger overrides PHP's default error handling to provide stack traces and enhanced logging * * @package cake * @subpackage cake.cake.libs */ class Debugger extends Object { /** * Holds a reference to errors generated by the application * * @var array * @access public */ var $errors = array(); /** * Contains the base URL for error code documentation * * @var string * @access public */ var $helpPath = null; /** * holds current output format * * @var string * @access private */ var $__outputFormat = 'js'; /** * holds current output data when outputFormat is false * * @var string * @access private */ var $__data = array(); /** * Constructor * */ function __construct() { $docRef = ini_get('docref_root'); if (empty($docRef)) { ini_set('docref_root', 'http://php.net/'); } if (!defined('E_RECOVERABLE_ERROR')) { define('E_RECOVERABLE_ERROR', 4096); } } /** * Gets a reference to the Debugger object instance * * @return object * @access public */ function &getInstance() { static $instance = array(); if (!isset($instance[0]) || !$instance[0]) { $instance[0] =& new Debugger(); if (Configure::read() > 0) { Configure::version(); // Make sure the core config is loaded $instance[0]->helpPath = Configure::read('Cake.Debugger.HelpPath'); } } return $instance[0]; } /** * formats and outputs the passed var */ function dump($var) { $_this = Debugger::getInstance(); pr($_this->exportVar($var)); } /** * Overrides PHP's default error handling * * @param integer $code Code of error * @param string $description Error description * @param string $file File on which error occurred * @param integer $line Line that triggered the error * @param array $context Context * @return boolean true if error was handled * @access public */ function handleError($code, $description, $file = null, $line = null, $context = null) { if (error_reporting() == 0 || $code === 2048) { return; } $_this = Debugger::getInstance(); if (empty($file)) { $file = '[internal]'; } if (empty($line)) { $line = '??'; } $file = $_this->trimPath($file); $info = compact('code', 'description', 'file', 'line'); if (!in_array($info, $_this->errors)) { $_this->errors[] = $info; } else { return; } $level = LOG_DEBUG; switch ($code) { case E_PARSE: case E_ERROR: case E_CORE_ERROR: case E_COMPILE_ERROR: case E_USER_ERROR: $error = 'Fatal Error'; $level = LOG_ERROR; break; case E_WARNING: case E_USER_WARNING: case E_COMPILE_WARNING: case E_RECOVERABLE_ERROR: $error = 'Warning'; $level = LOG_WARNING; break; case E_NOTICE: case E_USER_NOTICE: $error = 'Notice'; $level = LOG_NOTICE; break; default: return false; break; } $helpCode = null; if (!empty($_this->helpPath) && preg_match('/.*\[([0-9]+)\]$/', $description, $codes)) { if (isset($codes[1])) { $helpCode = $codes[1]; $description = trim(preg_replace('/\[[0-9]+\]$/', '', $description)); } } echo $_this->__output($level, $error, $code, $helpCode, $description, $file, $line, $context); if (Configure::read('log')) { CakeLog::write($level, "{$error} ({$code}): {$description} in [{$file}, line {$line}]"); } if ($error == 'Fatal Error') { die(); } return true; } /** * Outputs a stack trace with the given options * * @param array $options Format for outputting stack trace * @return string Formatted stack trace * @access public */ function trace($options = array()) { $options = array_merge(array( 'depth' => 999, 'format' => '', 'args' => false, 'start' => 0, 'scope' => null, 'exclude' => null ), $options ); $backtrace = debug_backtrace(); $back = array(); for ($i = $options['start']; $i < count($backtrace) && $i < $options['depth']; $i++) { $trace = array_merge( array( 'file' => '[internal]', 'line' => '??' ), $backtrace[$i] ); if (isset($backtrace[$i + 1])) { $next = array_merge( array( 'line' => '??', 'file' => '[internal]', 'class' => null, 'function' => '[main]' ), $backtrace[$i + 1] ); $function = $next['function']; if (!empty($next['class'])) { $function = $next['class'] . '::' . $function . '('; if ($options['args'] && isset($next['args'])) { $args = array(); foreach ($next['args'] as $arg) { $args[] = Debugger::exportVar($arg); } $function .= join(', ', $args); } $function .= ')'; } } else { $function = '[main]'; } if (in_array($function, array('call_user_func_array', 'trigger_error'))) { continue; } if ($options['format'] == 'points' && $trace['file'] != '[internal]') { $back[] = array('file' => $trace['file'], 'line' => $trace['line']); } elseif (empty($options['format'])) { $back[] = $function . ' - ' . Debugger::trimPath($trace['file']) . ', line ' . $trace['line']; } } if ($options['format'] == 'array' || $options['format'] == 'points') { return $back; } return join("\n", $back); } /** * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core * path with 'CORE' * * @param string $path Path to shorten * @return string Normalized path * @access public */ function trimPath($path) { if (!defined('CAKE_CORE_INCLUDE_PATH') || !defined('APP')) { return $path; } if (strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) { $path = str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path); } elseif (strpos($path, APP) === 0) { $path = str_replace(APP, 'APP' . DS, $path); } elseif (strpos($path, ROOT) === 0) { $path = str_replace(ROOT, 'ROOT', $path); } return $path; } /** * Grabs an excerpt from a file and highlights a given line of code * * @param string $file Absolute path to a PHP file * @param integer $line Line number to highlight * @param integer $context Number of lines of context to extract above and below $line * @return array Set of lines highlighted * @access public */ function excerpt($file, $line, $context = 2) { $data = $lines = array(); $data = @explode("\n", file_get_contents($file)); if (empty($data) || !isset($data[$line])) { return; } for ($i = $line - ($context + 1); $i < $line + $context; $i++) { if (!isset($data[$i])) { continue; } $string = str_replace(array("\r\n", "\n"), "", highlight_string($data[$i], true)); if ($i == $line) { $lines[] = '' . $string . ''; } else { $lines[] = $string; } } return $lines; } /** * Converts a variable to a string for debug output * * @param string $var Variable to convert * @return string Variable as a formatted string * @access public */ function exportVar($var, $recursion = 0) { $_this = Debugger::getInstance(); switch(strtolower(gettype($var))) { case 'boolean': return ife($var, 'true', 'false'); break; case 'integer': case 'double': return $var; break; case 'string': if (trim($var) == "") { return '"[empty string]"'; } return '"' . h($var) . '"'; break; case 'object': return get_class($var) . "\n" . $_this->__object($var); case 'array': $out = "array("; $vars = array(); foreach ($var as $key => $val) { if (is_numeric($key)) { $vars[] = "\n\t" . $_this->exportVar($val, $recursion - 1); } else { $vars[] = "\n\t" .$_this->exportVar($key) . ' => ' . $_this->exportVar($val, $recursion - 1); } } $n = null; if (count($vars) > 0) { $n = "\n"; } return $out . join(",", $vars) . "{$n})"; break; case 'resource': return strtolower(gettype($var)); break; case 'null': return 'null'; break; } } /** * Handles object conversion to debug string * * @param string $var Object to convert * @access private */ function __object($var) { static $history = array(); $serialized = serialize($var); array_push($history, $serialized); $out = array(); if(is_object($var)) { $className = get_class($var); $objectVars = get_object_vars($var); foreach($objectVars as $key => $value) { $inline = null; if(strpos($key, '_', 0) !== 0) { $inline = "$className::$key = "; } if(is_object($value) || is_array($value)) { $serialized = serialize($value); if(in_array($serialized, $history, true)) { $value = ife(is_object($value), "*RECURSION* -> " . get_class($value), "*RECURSION*"); } } if(in_array(gettype($value), array('boolean', 'integer', 'double', 'string', 'array', 'resource', 'object', 'null'))) { $out[] = "$className::$$key = " . Debugger::exportVar($value); } else { $out[] = "$className::$$key = " . var_export($value, true); } } $objectMethods = get_class_methods($className); foreach($objectMethods as $key => $value) { if(strpos($value, '_', 0) !== 0) { $out[] = "$className::$value()"; } } } array_pop($history); return join("\n", $out); } /** * Handles object conversion to debug string * * @param string $var Object to convert * @access protected */ function output($format = 'js') { $_this = Debugger::getInstance(); $data = null; if ($format === true && !empty($_this->__data)) { $data = $_this->__data; $_this->__data = array(); $format = false; } $_this->__outputFormat = $format; return $data; } /** * Handles object conversion to debug string * * @param string $var Object to convert * @access private */ function __output($level, $error, $code, $helpCode, $description, $file, $line, $kontext) { $_this = Debugger::getInstance(); $files = $_this->trace(array('start' => 2, 'format' => 'points')); $listing = $_this->excerpt($files[0]['file'], $files[0]['line'] - 1, 1); $trace = $_this->trace(array('start' => 2)); $context = array(); foreach ((array)$kontext as $var => $value) { $context[] = "\${$var}\t=\t" . $_this->exportVar($value, 1); } switch ($_this->__outputFormat) { default: case 'js': $link = "document.getElementById(\"CakeStackTrace" . count($_this->errors) . "\").style.display = (document.getElementById(\"CakeStackTrace" . count($_this->errors) . "\").style.display == \"none\" ? \"\" : \"none\")"; $out = "{$error} ({$code}): {$description} [{$file}, line {$line}]"; if (Configure::read() > 0) { debug($out, false, false); e(''); } break; case 'html': echo "
{$error} ({$code}) : {$description} [{$file}, line {$line}]
"; if (!empty($context)) { echo "Context:\n" .implode("\n", $context) . "\n"; } echo "
Context 

" . implode("\n", $context) . "

"; echo "
Trace 

" . $trace. "

"; break; case 'text': case 'txt': echo "{$error}: {$code} :: {$description} on line {$line} of {$file}\n"; if (!empty($context)) { echo "Context:\n" .implode("\n", $context) . "\n"; } echo "Trace:\n" . $trace; break; case 'log': $_this->log(compact('error', 'code', 'description', 'line', 'file', 'context', 'trace')); break; case false: $this->__data[] = compact('error', 'code', 'description', 'line', 'file', 'context', 'trace'); break; } } /** * Verify that the application's salt has been changed from the default value * * @access public */ function checkSessionKey() { if (Configure::read('Security.salt') == 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi') { 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); } } /** * Invokes the given debugger object as the current error handler, taking over control from the previous handler * in a stack-like hierarchy. * * @param object $debugger A reference to the Debugger object * @access public */ function invoke(&$debugger) { set_error_handler(array(&$debugger, 'handleError')); } } if (!defined('DISABLE_DEFAULT_ERROR_HANDLING')) { Debugger::invoke(Debugger::getInstance()); } ?>