debugger.php

Go to the documentation of this file.
00001 <?php
00002 /* SVN FILE: $Id: debugger_8php-source.html 580 2008-07-01 14:45:49Z gwoo $ */
00003 /**
00004  * Framework debugging and PHP error-handling class
00005  *
00006  * Provides enhanced logging, stack traces, and rendering debug views
00007  *
00008  * PHP versions 4 and 5
00009  *
00010  * CakePHP(tm) :  Rapid Development Framework <http://www.cakephp.org/>
00011  * Copyright 2005-2008, Cake Software Foundation, Inc.
00012  *                              1785 E. Sahara Avenue, Suite 490-204
00013  *                              Las Vegas, Nevada 89104
00014  *
00015  * Licensed under The MIT License
00016  * Redistributions of files must retain the above copyright notice.
00017  *
00018  * @filesource
00019  * @copyright       Copyright 2005-2008, Cake Software Foundation, Inc.
00020  * @link                http://www.cakefoundation.org/projects/info/cakephp CakePHP(tm) Project
00021  * @package         cake
00022  * @subpackage      cake.cake.libs
00023  * @since           CakePHP(tm) v 1.2.4560
00024  * @version         $Revision: 580 $
00025  * @modifiedby      $LastChangedBy: gwoo $
00026  * @lastmodified    $Date: 2008-07-01 09:45:49 -0500 (Tue, 01 Jul 2008) $
00027  * @license         http://www.opensource.org/licenses/mit-license.php The MIT License
00028  */
00029 /**
00030  * Included libraries.
00031  *
00032  */
00033     if (!class_exists('Object')) {
00034         uses('object');
00035     }
00036     if (!class_exists('CakeLog')) {
00037         uses('cake_log');
00038     }
00039 /**
00040  * Provide custom logging and error handling.
00041  *
00042  * Debugger overrides PHP's default error handling to provide stack traces and enhanced logging
00043  *
00044  * @package     cake
00045  * @subpackage  cake.cake.libs
00046  */
00047 class Debugger extends Object {
00048 
00049 /**
00050  * Holds a reference to errors generated by the application
00051  *
00052  * @var array
00053  * @access public
00054  */
00055     var $errors = array();
00056 /**
00057  * Contains the base URL for error code documentation
00058  *
00059  * @var string
00060  * @access public
00061  */
00062     var $helpPath = null;
00063 /**
00064  * holds current output format
00065  *
00066  * @var string
00067  * @access private
00068  */
00069     var $__outputFormat = 'js';
00070 /**
00071  * holds current output data when outputFormat is false
00072  *
00073  * @var string
00074  * @access private
00075  */
00076     var $__data = array();
00077 /**
00078  * Constructor
00079  *
00080  */
00081     function __construct() {
00082         $docRef = ini_get('docref_root');
00083         if (empty($docRef)) {
00084             ini_set('docref_root', 'http://php.net/');
00085         }
00086         if (!defined('E_RECOVERABLE_ERROR')) {
00087             define('E_RECOVERABLE_ERROR', 4096);
00088         }
00089     }
00090 /**
00091  * Gets a reference to the Debugger object instance
00092  *
00093  * @return object
00094  * @access public
00095  */
00096     function &getInstance() {
00097         static $instance = array();
00098 
00099         if (!isset($instance[0]) || !$instance[0]) {
00100             $instance[0] =& new Debugger();
00101             if (Configure::read() > 0) {
00102                 Configure::version(); // Make sure the core config is loaded
00103                 $instance[0]->helpPath = Configure::read('Cake.Debugger.HelpPath');
00104             }
00105         }
00106         return $instance[0];
00107     }
00108 /**
00109  * formats and outputs the passed var
00110 */
00111     function dump($var) {
00112         $_this = Debugger::getInstance();
00113         pr($_this->exportVar($var));
00114     }
00115 /**
00116  *  neatly logs a given var
00117 */
00118     function log($var, $level = LOG_DEBUG) {
00119         $_this = Debugger::getInstance();
00120         $trace = $_this->trace(array('start' => 1, 'depth' => 2, 'format' => 'array'));
00121         $source = null;
00122 
00123         if (is_object($trace[0]['object']) && isset($trace[0]['object']->_reporter->_test_stack)) {
00124             $stack = $trace[0]['object']->_reporter->_test_stack;
00125             $source = "[". $stack[0].", ". $stack[2] ."::" . $stack[3] ."()]\n";
00126         }
00127 
00128         CakeLog::write($level, $source . $_this->exportVar($var));
00129     }
00130 
00131 /**
00132  * Overrides PHP's default error handling
00133  *
00134  * @param integer $code Code of error
00135  * @param string $description Error description
00136  * @param string $file File on which error occurred
00137  * @param integer $line Line that triggered the error
00138  * @param array $context Context
00139  * @return boolean true if error was handled
00140  * @access public
00141  */
00142     function handleError($code, $description, $file = null, $line = null, $context = null) {
00143         if (error_reporting() == 0 || $code === 2048) {
00144             return;
00145         }
00146 
00147         $_this = Debugger::getInstance();
00148 
00149         if (empty($file)) {
00150             $file = '[internal]';
00151         }
00152         if (empty($line)) {
00153             $line = '??';
00154         }
00155         $file = $_this->trimPath($file);
00156 
00157         $info = compact('code', 'description', 'file', 'line');
00158         if (!in_array($info, $_this->errors)) {
00159             $_this->errors[] = $info;
00160         } else {
00161             return;
00162         }
00163 
00164         $level = LOG_DEBUG;
00165         switch ($code) {
00166             case E_PARSE:
00167             case E_ERROR:
00168             case E_CORE_ERROR:
00169             case E_COMPILE_ERROR:
00170             case E_USER_ERROR:
00171                 $error = 'Fatal Error';
00172                 $level = LOG_ERROR;
00173             break;
00174             case E_WARNING:
00175             case E_USER_WARNING:
00176             case E_COMPILE_WARNING:
00177             case E_RECOVERABLE_ERROR:
00178                 $error = 'Warning';
00179                 $level = LOG_WARNING;
00180             break;
00181             case E_NOTICE:
00182             case E_USER_NOTICE:
00183                 $error = 'Notice';
00184                 $level = LOG_NOTICE;
00185             break;
00186             default:
00187                 return false;
00188             break;
00189         }
00190 
00191         $helpCode = null;
00192         if (!empty($_this->helpPath) && preg_match('/.*\[([0-9]+)\]$/', $description, $codes)) {
00193             if (isset($codes[1])) {
00194                 $helpCode = $codes[1];
00195                 $description = trim(preg_replace('/\[[0-9]+\]$/', '', $description));
00196             }
00197         }
00198 
00199         echo $_this->__output($level, $error, $code, $helpCode, $description, $file, $line, $context);
00200 
00201         if (Configure::read('log')) {
00202             CakeLog::write($level, "{$error} ({$code}): {$description} in [{$file}, line {$line}]");
00203         }
00204 
00205         if ($error == 'Fatal Error') {
00206             die();
00207         }
00208         return true;
00209     }
00210 /**
00211  * Outputs a stack trace with the given options
00212  *
00213  * @param array $options Format for outputting stack trace
00214  * @return string Formatted stack trace
00215  * @access public
00216  */
00217     function trace($options = array()) {
00218         $options = array_merge(array(
00219                 'depth'     => 999,
00220                 'format'    => '',
00221                 'args'      => false,
00222                 'start'     => 0,
00223                 'scope'     => null,
00224                 'exclude'   => null
00225             ),
00226             $options
00227         );
00228 
00229         $backtrace = debug_backtrace();
00230         $back = array();
00231 
00232         for ($i = $options['start']; $i < count($backtrace) && $i < $options['depth']; $i++) {
00233             $trace = array_merge(
00234                 array(
00235                     'file' => '[internal]',
00236                     'line' => '??'
00237                 ),
00238                 $backtrace[$i]
00239             );
00240 
00241             if (isset($backtrace[$i + 1])) {
00242                 $next = array_merge(
00243                     array(
00244                         'line'      => '??',
00245                         'file'      => '[internal]',
00246                         'class'     => null,
00247                         'function'  => '[main]'
00248                     ),
00249                     $backtrace[$i + 1]
00250                 );
00251                 $function = $next['function'];
00252 
00253                 if (!empty($next['class'])) {
00254                     $function = $next['class'] . '::' . $function . '(';
00255                     if ($options['args'] && isset($next['args'])) {
00256                         $args = array();
00257                         foreach ($next['args'] as $arg) {
00258                             $args[] = Debugger::exportVar($arg);
00259                         }
00260                         $function .= join(', ', $args);
00261                     }
00262                     $function .= ')';
00263                 }
00264             } else {
00265                 $function = '[main]';
00266             }
00267             if (in_array($function, array('call_user_func_array', 'trigger_error'))) {
00268                 continue;
00269             }
00270             if ($options['format'] == 'points' && $trace['file'] != '[internal]') {
00271                 $back[] = array('file' => $trace['file'], 'line' => $trace['line']);
00272             } elseif (empty($options['format'])) {
00273                 $back[] = $function . ' - ' . Debugger::trimPath($trace['file']) . ', line ' . $trace['line'];
00274             } else {
00275                 $back[] = $trace;
00276             }
00277         }
00278 
00279         if ($options['format'] == 'array' || $options['format'] == 'points') {
00280             return $back;
00281         }
00282         return join("\n", $back);
00283     }
00284 /**
00285  * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core
00286  * path with 'CORE'
00287  *
00288  * @param string $path Path to shorten
00289  * @return string Normalized path
00290  * @access public
00291  */
00292     function trimPath($path) {
00293         if (!defined('CAKE_CORE_INCLUDE_PATH') || !defined('APP')) {
00294             return $path;
00295         }
00296 
00297         if (strpos($path, APP) === 0) {
00298             return str_replace(APP, 'APP' . DS, $path);
00299         } elseif (strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) {
00300             return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path);
00301         } elseif (strpos($path, ROOT) === 0) {
00302             return str_replace(ROOT, 'ROOT', $path);
00303         }
00304         $corePaths = Configure::corePaths('cake');
00305         foreach ($corePaths as $corePath) {
00306             if (strpos($path, $corePath) === 0) {
00307                 return str_replace($corePath, 'CORE' .DS . 'cake' .DS, $path);
00308             }
00309         }
00310         return $path;
00311     }
00312 /**
00313  * Grabs an excerpt from a file and highlights a given line of code
00314  *
00315  * @param string $file Absolute path to a PHP file
00316  * @param integer $line Line number to highlight
00317  * @param integer $context Number of lines of context to extract above and below $line
00318  * @return array Set of lines highlighted
00319  * @access public
00320  */
00321     function excerpt($file, $line, $context = 2) {
00322         $data = $lines = array();
00323         $data = @explode("\n", file_get_contents($file));
00324 
00325         if (empty($data) || !isset($data[$line])) {
00326             return;
00327         }
00328         for ($i = $line - ($context + 1); $i < $line + $context; $i++) {
00329             if (!isset($data[$i])) {
00330                 continue;
00331             }
00332             $string = str_replace(array("\r\n", "\n"), "", highlight_string($data[$i], true));
00333             if ($i == $line) {
00334                 $lines[] = '<span class="code-highlight">' . $string . '</span>';
00335             } else {
00336                 $lines[] = $string;
00337             }
00338         }
00339         return $lines;
00340     }
00341 /**
00342  * Converts a variable to a string for debug output
00343  *
00344  * @param string $var Variable to convert
00345  * @return string Variable as a formatted string
00346  * @access public
00347  */
00348     function exportVar($var, $recursion = 0) {
00349         $_this =  Debugger::getInstance();
00350         switch(strtolower(gettype($var))) {
00351             case 'boolean':
00352                 return ife($var, 'true', 'false');
00353             break;
00354             case 'integer':
00355             case 'double':
00356                 return $var;
00357             break;
00358             case 'string':
00359                 if (trim($var) == "") {
00360                     return '""';
00361                 }
00362                 return '"' . h($var) . '"';
00363             break;
00364             case 'object':
00365                 return get_class($var) . "\n" . $_this->__object($var);
00366             case 'array':
00367                 $out = "array(";
00368                 $vars = array();
00369                 foreach ($var as $key => $val) {
00370                     if ($recursion >= 0) {
00371                         if (is_numeric($key)) {
00372                             $vars[] = "\n\t" . $_this->exportVar($val, $recursion - 1);
00373                         } else {
00374                             $vars[] = "\n\t" .$_this->exportVar($key, $recursion - 1)
00375                                         . ' => ' . $_this->exportVar($val, $recursion - 1);
00376                         }
00377                     }
00378                 }
00379                 $n = null;
00380                 if (count($vars) > 0) {
00381                     $n = "\n";
00382                 }
00383                 return $out . join(",", $vars) . "{$n})";
00384             break;
00385             case 'resource':
00386                 return strtolower(gettype($var));
00387             break;
00388             case 'null':
00389                 return 'null';
00390             break;
00391         }
00392     }
00393 /**
00394  * Handles object conversion to debug string
00395  *
00396  * @param string $var Object to convert
00397  * @access private
00398  */
00399     function __object($var) {
00400         $out = array();
00401 
00402         if(is_object($var)) {
00403             $className = get_class($var);
00404             $objectVars = get_object_vars($var);
00405 
00406             foreach($objectVars as $key => $value) {
00407                 if(is_object($value)) {
00408                     $value = get_class($value) . ' object';
00409                 } elseif (is_array($value)) {
00410                     $value = 'array';
00411                 } elseif ($value === null) {
00412                     $value = 'NULL';
00413                 } elseif (in_array(gettype($value), array('boolean', 'integer', 'double', 'string', 'array', 'resource'))) {
00414                     $value = Debugger::exportVar($value);
00415                 }
00416                 $out[] = "$className::$$key = " . $value;
00417             }
00418         }
00419         return join("\n", $out);
00420     }
00421 /**
00422  * Handles object conversion to debug string
00423  *
00424  * @param string $var Object to convert
00425  * @access protected
00426  */
00427     function output($format = 'js') {
00428         $_this = Debugger::getInstance();
00429         $data = null;
00430 
00431         if ($format === true && !empty($_this->__data)) {
00432             $data = $_this->__data;
00433             $_this->__data = array();
00434             $format = false;
00435         }
00436         $_this->__outputFormat = $format;
00437 
00438         return $data;
00439     }
00440 /**
00441  * Handles object conversion to debug string
00442  *
00443  * @param string $var Object to convert
00444  * @access private
00445  */
00446     function __output($level, $error, $code, $helpCode, $description, $file, $line, $kontext) {
00447         $_this = Debugger::getInstance();
00448 
00449         $files = $_this->trace(array('start' => 2, 'format' => 'points'));
00450         $listing = $_this->excerpt($files[0]['file'], $files[0]['line'] - 1, 1);
00451         $trace = $_this->trace(array('start' => 2, 'depth' => '20'));
00452         $context = array();
00453 
00454         foreach ((array)$kontext as $var => $value) {
00455             $context[] = "\${$var}\t=\t" . $_this->exportVar($value, 1);
00456         }
00457 
00458         switch ($_this->__outputFormat) {
00459             default:
00460             case 'js':
00461                 $link = "document.getElementById(\"CakeStackTrace" . count($_this->errors) . "\").style.display = (document.getElementById(\"CakeStackTrace" . count($_this->errors) . "\").style.display == \"none\" ? \"\" : \"none\")";
00462                 $out = "<a href='javascript:void(0);' onclick='{$link}'><b>{$error}</b> ({$code})</a>: {$description} [<b>{$file}</b>, line <b>{$line}</b>]";
00463                 if (Configure::read() > 0) {
00464                     debug($out, false, false);
00465                     e('<div id="CakeStackTrace' . count($_this->errors) . '" class="cake-stack-trace" style="display: none;">');
00466                         $link = "document.getElementById(\"CakeErrorCode" . count($_this->errors) . "\").style.display = (document.getElementById(\"CakeErrorCode" . count($_this->errors) . "\").style.display == \"none\" ? \"\" : \"none\")";
00467                         e("<a href='javascript:void(0);' onclick='{$link}'>Code</a>");
00468 
00469                         if (!empty($context)) {
00470                             $link = "document.getElementById(\"CakeErrorContext" . count($_this->errors) . "\").style.display = (document.getElementById(\"CakeErrorContext" . count($_this->errors) . "\").style.display == \"none\" ? \"\" : \"none\")";
00471                             e(" | <a href='javascript:void(0);' onclick='{$link}'>Context</a>");
00472 
00473                             if (!empty($helpCode)) {
00474                                 e(" | <a href='{$_this->helpPath}{$helpCode}' target='blank'>Help</a>");
00475                             }
00476 
00477                             e("<pre id=\"CakeErrorContext" . count($_this->errors) . "\" class=\"cake-context\" style=\"display: none;\">");
00478                             e(implode("\n", $context));
00479                             e("</pre>");
00480                         }
00481 
00482                         if (!empty($listing)) {
00483                             e("<div id=\"CakeErrorCode" . count($_this->errors) . "\" class=\"cake-code-dump\" style=\"display: none;\">");
00484                                 pr(implode("\n", $listing) . "\n", false);
00485                             e('</div>');
00486                         }
00487                         pr($trace, false);
00488                     e('</div>');
00489                 }
00490             break;
00491             case 'html':
00492                 echo "<pre class=\"cake-debug\"><b>{$error}</b> ({$code}) : {$description} [<b>{$file}</b>, line <b>{$line}]</b></pre>";
00493                 if (!empty($context)) {
00494                     echo "Context:\n" .implode("\n", $context) . "\n";
00495                 }
00496                 echo "<pre class=\"cake-debug context\"><b>Context</b> <p>" . implode("\n", $context) . "</p></pre>";
00497                 echo "<pre class=\"cake-debug trace\"><b>Trace</b> <p>" . $trace. "</p></pre>";
00498             break;
00499             case 'text':
00500             case 'txt':
00501                 echo "{$error}: {$code} :: {$description} on line {$line} of {$file}\n";
00502                 if (!empty($context)) {
00503                     echo "Context:\n" .implode("\n", $context) . "\n";
00504                 }
00505                 echo "Trace:\n" . $trace;
00506             break;
00507             case 'log':
00508                 $_this->log(compact('error', 'code', 'description', 'line', 'file', 'context', 'trace'));
00509             break;
00510             case false:
00511                 $this->__data[] = compact('error', 'code', 'description', 'line', 'file', 'context', 'trace');
00512             break;
00513         }
00514     }
00515 /**
00516  * Verify that the application's salt has been changed from the default value
00517  *
00518  * @access public
00519  */
00520     function checkSessionKey() {
00521         if (Configure::read('Security.salt') == 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi') {
00522             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);
00523         }
00524     }
00525 /**
00526  * Invokes the given debugger object as the current error handler, taking over control from the previous handler
00527  * in a stack-like hierarchy.
00528  *
00529  * @param object $debugger A reference to the Debugger object
00530  * @access public
00531  */
00532     function invoke(&$debugger) {
00533         set_error_handler(array(&$debugger, 'handleError'));
00534     }
00535 }
00536 
00537 if (!defined('DISABLE_DEFAULT_ERROR_HANDLING')) {
00538     Debugger::invoke(Debugger::getInstance());
00539 }
00540 
00541 ?>