source: Dev/branches/cakephp/cake/libs/debugger.php @ 126

Last change on this file since 126 was 126, checked in by fpvanagthoven, 14 years ago

Cakephp branch.

File size: 19.9 KB
Line 
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-2011, 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-2011, 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 */
27if (!class_exists('Object')) {
28        require_once LIBS . 'object.php';
29}
30if (!class_exists('CakeLog')) {
31        require_once LIBS . 'cake_log.php';
32}
33if (!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 */
46class 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                                $var = array_merge($var,  array_intersect_key(array(
503                                        'password' => '*****',
504                                        'login'  => '*****',
505                                        'host' => '*****',
506                                        'database' => '*****',
507                                        'port' => '*****',
508                                        'prefix' => '*****',
509                                        'schema' => '*****'
510                                ), $var));
511
512                                $out = "array(";
513                                $vars = array();
514                                foreach ($var as $key => $val) {
515                                        if ($recursion >= 0) {
516                                                if (is_numeric($key)) {
517                                                        $vars[] = "\n\t" . $_this->exportVar($val, $recursion - 1);
518                                                } else {
519                                                        $vars[] = "\n\t" .$_this->exportVar($key, $recursion - 1)
520                                                                                . ' => ' . $_this->exportVar($val, $recursion - 1);
521                                                }
522                                        }
523                                }
524                                $n = null;
525                                if (!empty($vars)) {
526                                        $n = "\n";
527                                }
528                                return $out . implode(",", $vars) . "{$n})";
529                        break;
530                        case 'resource':
531                                return strtolower(gettype($var));
532                        break;
533                        case 'null':
534                                return 'null';
535                        break;
536                }
537        }
538
539/**
540 * Handles object to string conversion.
541 *
542 * @param string $var Object to convert
543 * @return string
544 * @access private
545 * @see Debugger::exportVar()
546 */
547        function __object($var) {
548                $out = array();
549
550                if (is_object($var)) {
551                        $className = get_class($var);
552                        $objectVars = get_object_vars($var);
553
554                        foreach ($objectVars as $key => $value) {
555                                if (is_object($value)) {
556                                        $value = get_class($value) . ' object';
557                                } elseif (is_array($value)) {
558                                        $value = 'array';
559                                } elseif ($value === null) {
560                                        $value = 'NULL';
561                                } elseif (in_array(gettype($value), array('boolean', 'integer', 'double', 'string', 'array', 'resource'))) {
562                                        $value = Debugger::exportVar($value);
563                                }
564                                $out[] = "$className::$$key = " . $value;
565                        }
566                }
567                return implode("\n", $out);
568        }
569
570/**
571 * Switches output format, updates format strings
572 *
573 * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for
574 *    straight HTML output, or 'txt' for unformatted text.
575 * @param array $strings Template strings to be used for the output format.
576 * @access protected
577 */
578        function output($format = null, $strings = array()) {
579                $_this =& Debugger::getInstance();
580                $data = null;
581
582                if (is_null($format)) {
583                        return $_this->_outputFormat;
584                }
585
586                if (!empty($strings)) {
587                        if (isset($_this->_templates[$format])) {
588                                if (isset($strings['links'])) {
589                                        $_this->_templates[$format]['links'] = array_merge(
590                                                $_this->_templates[$format]['links'],
591                                                $strings['links']
592                                        );
593                                        unset($strings['links']);
594                                }
595                                $_this->_templates[$format] = array_merge($_this->_templates[$format], $strings);
596                        } else {
597                                $_this->_templates[$format] = $strings;
598                        }
599                        return $_this->_templates[$format];
600                }
601
602                if ($format === true && !empty($_this->_data)) {
603                        $data = $_this->_data;
604                        $_this->_data = array();
605                        $format = false;
606                }
607                $_this->_outputFormat = $format;
608
609                return $data;
610        }
611
612/**
613 * Renders error messages
614 *
615 * @param array $data Data about the current error
616 * @access private
617 */
618        function _output($data = array()) {
619                $defaults = array(
620                        'level' => 0,
621                        'error' => 0,
622                        'code' => 0,
623                        'helpID' => null,
624                        'description' => '',
625                        'file' => '',
626                        'line' => 0,
627                        'context' => array()
628                );
629                $data += $defaults;
630
631                $files = $this->trace(array('start' => 2, 'format' => 'points'));
632                $code = $this->excerpt($files[0]['file'], $files[0]['line'] - 1, 1);
633                $trace = $this->trace(array('start' => 2, 'depth' => '20'));
634                $insertOpts = array('before' => '{:', 'after' => '}');
635                $context = array();
636                $links = array();
637                $info = '';
638
639                foreach ((array)$data['context'] as $var => $value) {
640                        $context[] = "\${$var}\t=\t" . $this->exportVar($value, 1);
641                }
642
643                switch ($this->_outputFormat) {
644                        case false:
645                                $this->_data[] = compact('context', 'trace') + $data;
646                                return;
647                        case 'log':
648                                $this->log(compact('context', 'trace') + $data);
649                                return;
650                }
651
652                if (empty($this->_outputFormat) || !isset($this->_templates[$this->_outputFormat])) {
653                        $this->_outputFormat = 'js';
654                }
655
656                $data['id'] = 'cakeErr' . count($this->errors);
657                $tpl = array_merge($this->_templates['base'], $this->_templates[$this->_outputFormat]);
658                $insert = array('context' => join("\n", $context), 'helpPath' => $this->helpPath) + $data;
659
660                $detect = array('help' => 'helpID', 'context' => 'context');
661
662                if (isset($tpl['links'])) {
663                        foreach ($tpl['links'] as $key => $val) {
664                                if (isset($detect[$key]) && empty($insert[$detect[$key]])) {
665                                        continue;
666                                }
667                                $links[$key] = String::insert($val, $insert, $insertOpts);
668                        }
669                }
670
671                foreach (array('code', 'context', 'trace') as $key) {
672                        if (empty($$key) || !isset($tpl[$key])) {
673                                continue;
674                        }
675                        if (is_array($$key)) {
676                                $$key = join("\n", $$key);
677                        }
678                        $info .= String::insert($tpl[$key], compact($key) + $insert, $insertOpts);
679                }
680                $links = join(' | ', $links);
681                unset($data['context']);
682
683                echo String::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts);
684        }
685
686/**
687 * Verifies that the application's salt and cipher seed value has been changed from the default value.
688 *
689 * @access public
690 * @static
691 */
692        function checkSecurityKeys() {
693                if (Configure::read('Security.salt') == 'DYhG93b0qyJfIxfs2guVoUubWwvniR2G0FgaC9mi') {
694                        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);
695                }
696
697                if (Configure::read('Security.cipherSeed') === '76859309657453542496749683645') {
698                        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);
699                }
700        }
701
702/**
703 * Invokes the given debugger object as the current error handler, taking over control from the
704 * previous handler in a stack-like hierarchy.
705 *
706 * @param object $debugger A reference to the Debugger object
707 * @access public
708 * @static
709 * @link http://book.cakephp.org/view/1191/Using-the-Debugger-Class
710 */
711        function invoke(&$debugger) {
712                set_error_handler(array(&$debugger, 'handleError'));
713        }
714}
715
716if (!defined('DISABLE_DEFAULT_ERROR_HANDLING')) {
717        Debugger::invoke(Debugger::getInstance());
718}
Note: See TracBrowser for help on using the repository browser.