Debugger.php 18 KB

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