PageRenderTime 58ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Error/Debugger.php

http://github.com/cakephp/cakephp
PHP | 1088 lines | 621 code | 97 blank | 370 comment | 85 complexity | 3d455c4f3159ab6afac196c9d350e8b4 MD5 | raw file
Possible License(s): JSON
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 1.2.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Error;
  17. use Cake\Core\Configure;
  18. use Cake\Core\InstanceConfigTrait;
  19. use Cake\Error\Debug\ArrayItemNode;
  20. use Cake\Error\Debug\ArrayNode;
  21. use Cake\Error\Debug\ClassNode;
  22. use Cake\Error\Debug\ConsoleFormatter;
  23. use Cake\Error\Debug\DebugContext;
  24. use Cake\Error\Debug\FormatterInterface;
  25. use Cake\Error\Debug\HtmlFormatter;
  26. use Cake\Error\Debug\NodeInterface;
  27. use Cake\Error\Debug\PropertyNode;
  28. use Cake\Error\Debug\ReferenceNode;
  29. use Cake\Error\Debug\ScalarNode;
  30. use Cake\Error\Debug\SpecialNode;
  31. use Cake\Error\Debug\TextFormatter;
  32. use Cake\Log\Log;
  33. use Cake\Utility\Hash;
  34. use Cake\Utility\Security;
  35. use Cake\Utility\Text;
  36. use Closure;
  37. use Exception;
  38. use InvalidArgumentException;
  39. use ReflectionObject;
  40. use ReflectionProperty;
  41. use RuntimeException;
  42. use Throwable;
  43. /**
  44. * Provide custom logging and error handling.
  45. *
  46. * Debugger extends PHP's default error handling and gives
  47. * simpler to use more powerful interfaces.
  48. *
  49. * @link https://book.cakephp.org/4/en/development/debugging.html#namespace-Cake\Error
  50. */
  51. class Debugger
  52. {
  53. use InstanceConfigTrait;
  54. /**
  55. * Default configuration
  56. *
  57. * @var array<string, mixed>
  58. */
  59. protected $_defaultConfig = [
  60. 'outputMask' => [],
  61. 'exportFormatter' => null,
  62. 'editor' => 'phpstorm',
  63. ];
  64. /**
  65. * The current output format.
  66. *
  67. * @var string
  68. */
  69. protected $_outputFormat = 'js';
  70. /**
  71. * Templates used when generating trace or error strings. Can be global or indexed by the format
  72. * value used in $_outputFormat.
  73. *
  74. * @var array<string, array<string, mixed>>
  75. */
  76. protected $_templates = [
  77. 'log' => [
  78. 'trace' => '{:reference} - {:path}, line {:line}',
  79. 'error' => '{:error} ({:code}): {:description} in [{:file}, line {:line}]',
  80. ],
  81. 'js' => [
  82. 'error' => '',
  83. 'info' => '',
  84. 'trace' => '<pre class="stack-trace">{:trace}</pre>',
  85. 'code' => '',
  86. 'context' => '',
  87. 'links' => [],
  88. 'escapeContext' => true,
  89. ],
  90. 'html' => [
  91. 'trace' => '<pre class="cake-error trace"><b>Trace</b> <p>{:trace}</p></pre>',
  92. 'context' => '<pre class="cake-error context"><b>Context</b> <p>{:context}</p></pre>',
  93. 'escapeContext' => true,
  94. ],
  95. 'txt' => [
  96. 'error' => "{:error}: {:code} :: {:description} on line {:line} of {:path}\n{:info}",
  97. 'code' => '',
  98. 'info' => '',
  99. ],
  100. 'base' => [
  101. 'traceLine' => '{:reference} - {:path}, line {:line}',
  102. 'trace' => "Trace:\n{:trace}\n",
  103. 'context' => "Context:\n{:context}\n",
  104. ],
  105. ];
  106. /**
  107. * A map of editors to their link templates.
  108. *
  109. * @var array<string, string|callable>
  110. */
  111. protected $editors = [
  112. 'atom' => 'atom://core/open/file?filename={file}&line={line}',
  113. 'emacs' => 'emacs://open?url=file://{file}&line={line}',
  114. 'macvim' => 'mvim://open/?url=file://{file}&line={line}',
  115. 'phpstorm' => 'phpstorm://open?file={file}&line={line}',
  116. 'sublime' => 'subl://open?url=file://{file}&line={line}',
  117. 'textmate' => 'txmt://open?url=file://{file}&line={line}',
  118. 'vscode' => 'vscode://file/{file}:{line}',
  119. ];
  120. /**
  121. * Holds current output data when outputFormat is false.
  122. *
  123. * @var array
  124. */
  125. protected $_data = [];
  126. /**
  127. * Constructor.
  128. */
  129. public function __construct()
  130. {
  131. $docRef = ini_get('docref_root');
  132. if (empty($docRef) && function_exists('ini_set')) {
  133. ini_set('docref_root', 'https://secure.php.net/');
  134. }
  135. if (!defined('E_RECOVERABLE_ERROR')) {
  136. define('E_RECOVERABLE_ERROR', 4096);
  137. }
  138. $config = array_intersect_key((array)Configure::read('Debugger'), $this->_defaultConfig);
  139. $this->setConfig($config);
  140. $e = '<pre class="cake-error">';
  141. $e .= '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-trace\')';
  142. $e .= '.style.display = (document.getElementById(\'{:id}-trace\').style.display == ';
  143. $e .= '\'none\' ? \'\' : \'none\');"><b>{:error}</b> ({:code})</a>: {:description} ';
  144. $e .= '[<b>{:path}</b>, line <b>{:line}</b>]';
  145. $e .= '<div id="{:id}-trace" class="cake-stack-trace" style="display: none;">';
  146. $e .= '{:links}{:info}</div>';
  147. $e .= '</pre>';
  148. $this->_templates['js']['error'] = $e;
  149. $t = '<div id="{:id}-trace" class="cake-stack-trace" style="display: none;">';
  150. $t .= '{:context}{:code}{:trace}</div>';
  151. $this->_templates['js']['info'] = $t;
  152. $links = [];
  153. $link = '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-code\')';
  154. $link .= '.style.display = (document.getElementById(\'{:id}-code\').style.display == ';
  155. $link .= '\'none\' ? \'\' : \'none\')">Code</a>';
  156. $links['code'] = $link;
  157. $link = '<a href="javascript:void(0);" onclick="document.getElementById(\'{:id}-context\')';
  158. $link .= '.style.display = (document.getElementById(\'{:id}-context\').style.display == ';
  159. $link .= '\'none\' ? \'\' : \'none\')">Context</a>';
  160. $links['context'] = $link;
  161. $this->_templates['js']['links'] = $links;
  162. $this->_templates['js']['context'] = '<pre id="{:id}-context" class="cake-context cake-debug" ';
  163. $this->_templates['js']['context'] .= 'style="display: none;">{:context}</pre>';
  164. $this->_templates['js']['code'] = '<pre id="{:id}-code" class="cake-code-dump" ';
  165. $this->_templates['js']['code'] .= 'style="display: none;">{:code}</pre>';
  166. $e = '<pre class="cake-error"><b>{:error}</b> ({:code}) : {:description} ';
  167. $e .= '[<b>{:path}</b>, line <b>{:line}]</b></pre>';
  168. $this->_templates['html']['error'] = $e;
  169. $this->_templates['html']['context'] = '<pre class="cake-context cake-debug"><b>Context</b> ';
  170. $this->_templates['html']['context'] .= '<p>{:context}</p></pre>';
  171. }
  172. /**
  173. * Returns a reference to the Debugger singleton object instance.
  174. *
  175. * @param string|null $class Class name.
  176. * @return static
  177. */
  178. public static function getInstance(?string $class = null)
  179. {
  180. static $instance = [];
  181. if (!empty($class)) {
  182. if (!$instance || strtolower($class) !== strtolower(get_class($instance[0]))) {
  183. $instance[0] = new $class();
  184. }
  185. }
  186. if (!$instance) {
  187. $instance[0] = new Debugger();
  188. }
  189. return $instance[0];
  190. }
  191. /**
  192. * Read or write configuration options for the Debugger instance.
  193. *
  194. * @param array<string, mixed>|string|null $key The key to get/set, or a complete array of configs.
  195. * @param mixed|null $value The value to set.
  196. * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
  197. * @return mixed Config value being read, or the object itself on write operations.
  198. * @throws \Cake\Core\Exception\CakeException When trying to set a key that is invalid.
  199. */
  200. public static function configInstance($key = null, $value = null, bool $merge = true)
  201. {
  202. if ($key === null) {
  203. return static::getInstance()->getConfig($key);
  204. }
  205. if (is_array($key) || func_num_args() >= 2) {
  206. return static::getInstance()->setConfig($key, $value, $merge);
  207. }
  208. return static::getInstance()->getConfig($key);
  209. }
  210. /**
  211. * Reads the current output masking.
  212. *
  213. * @return array<string, string>
  214. */
  215. public static function outputMask(): array
  216. {
  217. return static::configInstance('outputMask');
  218. }
  219. /**
  220. * Sets configurable masking of debugger output by property name and array key names.
  221. *
  222. * ### Example
  223. *
  224. * Debugger::setOutputMask(['password' => '[*************]');
  225. *
  226. * @param array<string, string> $value An array where keys are replaced by their values in output.
  227. * @param bool $merge Whether to recursively merge or overwrite existing config, defaults to true.
  228. * @return void
  229. */
  230. public static function setOutputMask(array $value, bool $merge = true): void
  231. {
  232. static::configInstance('outputMask', $value, $merge);
  233. }
  234. /**
  235. * Add an editor link format
  236. *
  237. * Template strings can use the `{file}` and `{line}` placeholders.
  238. * Closures templates must return a string, and accept two parameters:
  239. * The file and line.
  240. *
  241. * @param string $name The name of the editor.
  242. * @param \Closure|string $template The string template or closure
  243. * @return void
  244. */
  245. public static function addEditor(string $name, $template): void
  246. {
  247. $instance = static::getInstance();
  248. if (!is_string($template) && !($template instanceof Closure)) {
  249. $type = getTypeName($template);
  250. throw new RuntimeException("Invalid editor type of `{$type}`. Expected string or Closure.");
  251. }
  252. $instance->editors[$name] = $template;
  253. }
  254. /**
  255. * Choose the editor link style you want to use.
  256. *
  257. * @param string $name The editor name.
  258. * @return void
  259. */
  260. public static function setEditor(string $name): void
  261. {
  262. $instance = static::getInstance();
  263. if (!isset($instance->editors[$name])) {
  264. $known = implode(', ', array_keys($instance->editors));
  265. throw new RuntimeException("Unknown editor `{$name}`. Known editors are {$known}");
  266. }
  267. $instance->setConfig('editor', $name);
  268. }
  269. /**
  270. * Get a formatted URL for the active editor.
  271. *
  272. * @param string $file The file to create a link for.
  273. * @param int $line The line number to create a link for.
  274. * @return string The formatted URL.
  275. */
  276. public static function editorUrl(string $file, int $line): string
  277. {
  278. $instance = static::getInstance();
  279. $editor = $instance->getConfig('editor');
  280. if (!isset($instance->editors[$editor])) {
  281. throw new RuntimeException("Cannot format editor URL `{$editor}` is not a known editor.");
  282. }
  283. $template = $instance->editors[$editor];
  284. if (is_string($template)) {
  285. return str_replace(['{file}', '{line}'], [$file, (string)$line], $template);
  286. }
  287. return $template($file, $line);
  288. }
  289. /**
  290. * Recursively formats and outputs the contents of the supplied variable.
  291. *
  292. * @param mixed $var The variable to dump.
  293. * @param int $maxDepth The depth to output to. Defaults to 3.
  294. * @return void
  295. * @see \Cake\Error\Debugger::exportVar()
  296. * @link https://book.cakephp.org/4/en/development/debugging.html#outputting-values
  297. */
  298. public static function dump($var, int $maxDepth = 3): void
  299. {
  300. pr(static::exportVar($var, $maxDepth));
  301. }
  302. /**
  303. * Creates an entry in the log file. The log entry will contain a stack trace from where it was called.
  304. * as well as export the variable using exportVar. By default, the log is written to the debug log.
  305. *
  306. * @param mixed $var Variable or content to log.
  307. * @param string|int $level Type of log to use. Defaults to 'debug'.
  308. * @param int $maxDepth The depth to output to. Defaults to 3.
  309. * @return void
  310. */
  311. public static function log($var, $level = 'debug', int $maxDepth = 3): void
  312. {
  313. /** @var string $source */
  314. $source = static::trace(['start' => 1]);
  315. $source .= "\n";
  316. Log::write(
  317. $level,
  318. "\n" . $source . static::exportVarAsPlainText($var, $maxDepth)
  319. );
  320. }
  321. /**
  322. * Outputs a stack trace based on the supplied options.
  323. *
  324. * ### Options
  325. *
  326. * - `depth` - The number of stack frames to return. Defaults to 999
  327. * - `format` - The format you want the return. Defaults to the currently selected format. If
  328. * format is 'array' or 'points' the return will be an array.
  329. * - `args` - Should arguments for functions be shown? If true, the arguments for each method call
  330. * will be displayed.
  331. * - `start` - The stack frame to start generating a trace from. Defaults to 0
  332. *
  333. * @param array<string, mixed> $options Format for outputting stack trace.
  334. * @return array|string Formatted stack trace.
  335. * @link https://book.cakephp.org/4/en/development/debugging.html#generating-stack-traces
  336. */
  337. public static function trace(array $options = [])
  338. {
  339. return Debugger::formatTrace(debug_backtrace(), $options);
  340. }
  341. /**
  342. * Formats a stack trace based on the supplied options.
  343. *
  344. * ### Options
  345. *
  346. * - `depth` - The number of stack frames to return. Defaults to 999
  347. * - `format` - The format you want the return. Defaults to the currently selected format. If
  348. * format is 'array' or 'points' the return will be an array.
  349. * - `args` - Should arguments for functions be shown? If true, the arguments for each method call
  350. * will be displayed.
  351. * - `start` - The stack frame to start generating a trace from. Defaults to 0
  352. *
  353. * @param \Throwable|array $backtrace Trace as array or an exception object.
  354. * @param array<string, mixed> $options Format for outputting stack trace.
  355. * @return array|string Formatted stack trace.
  356. * @link https://book.cakephp.org/4/en/development/debugging.html#generating-stack-traces
  357. */
  358. public static function formatTrace($backtrace, array $options = [])
  359. {
  360. if ($backtrace instanceof Throwable) {
  361. $backtrace = $backtrace->getTrace();
  362. }
  363. $self = Debugger::getInstance();
  364. $defaults = [
  365. 'depth' => 999,
  366. 'format' => $self->_outputFormat,
  367. 'args' => false,
  368. 'start' => 0,
  369. 'scope' => null,
  370. 'exclude' => ['call_user_func_array', 'trigger_error'],
  371. ];
  372. $options = Hash::merge($defaults, $options);
  373. $count = count($backtrace);
  374. $back = [];
  375. $_trace = [
  376. 'line' => '??',
  377. 'file' => '[internal]',
  378. 'class' => null,
  379. 'function' => '[main]',
  380. ];
  381. for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) {
  382. $trace = $backtrace[$i] + ['file' => '[internal]', 'line' => '??'];
  383. $signature = $reference = '[main]';
  384. if (isset($backtrace[$i + 1])) {
  385. $next = $backtrace[$i + 1] + $_trace;
  386. $signature = $reference = $next['function'];
  387. if (!empty($next['class'])) {
  388. $signature = $next['class'] . '::' . $next['function'];
  389. $reference = $signature . '(';
  390. if ($options['args'] && isset($next['args'])) {
  391. $args = [];
  392. foreach ($next['args'] as $arg) {
  393. $args[] = Debugger::exportVar($arg);
  394. }
  395. $reference .= implode(', ', $args);
  396. }
  397. $reference .= ')';
  398. }
  399. }
  400. if (in_array($signature, $options['exclude'], true)) {
  401. continue;
  402. }
  403. if ($options['format'] === 'points' && $trace['file'] !== '[internal]') {
  404. $back[] = ['file' => $trace['file'], 'line' => $trace['line']];
  405. } elseif ($options['format'] === 'array') {
  406. $back[] = $trace;
  407. } else {
  408. if (isset($self->_templates[$options['format']]['traceLine'])) {
  409. $tpl = $self->_templates[$options['format']]['traceLine'];
  410. } else {
  411. $tpl = $self->_templates['base']['traceLine'];
  412. }
  413. $trace['path'] = static::trimPath($trace['file']);
  414. $trace['reference'] = $reference;
  415. unset($trace['object'], $trace['args']);
  416. $back[] = Text::insert($tpl, $trace, ['before' => '{:', 'after' => '}']);
  417. }
  418. }
  419. if ($options['format'] === 'array' || $options['format'] === 'points') {
  420. return $back;
  421. }
  422. /** @psalm-suppress InvalidArgument */
  423. return implode("\n", $back);
  424. }
  425. /**
  426. * Shortens file paths by replacing the application base path with 'APP', and the CakePHP core
  427. * path with 'CORE'.
  428. *
  429. * @param string $path Path to shorten.
  430. * @return string Normalized path
  431. */
  432. public static function trimPath(string $path): string
  433. {
  434. if (defined('APP') && strpos($path, APP) === 0) {
  435. return str_replace(APP, 'APP/', $path);
  436. }
  437. if (defined('CAKE_CORE_INCLUDE_PATH') && strpos($path, CAKE_CORE_INCLUDE_PATH) === 0) {
  438. return str_replace(CAKE_CORE_INCLUDE_PATH, 'CORE', $path);
  439. }
  440. if (defined('ROOT') && strpos($path, ROOT) === 0) {
  441. return str_replace(ROOT, 'ROOT', $path);
  442. }
  443. return $path;
  444. }
  445. /**
  446. * Grabs an excerpt from a file and highlights a given line of code.
  447. *
  448. * Usage:
  449. *
  450. * ```
  451. * Debugger::excerpt('/path/to/file', 100, 4);
  452. * ```
  453. *
  454. * The above would return an array of 8 items. The 4th item would be the provided line,
  455. * and would be wrapped in `<span class="code-highlight"></span>`. All the lines
  456. * are processed with highlight_string() as well, so they have basic PHP syntax highlighting
  457. * applied.
  458. *
  459. * @param string $file Absolute path to a PHP file.
  460. * @param int $line Line number to highlight.
  461. * @param int $context Number of lines of context to extract above and below $line.
  462. * @return array<string> Set of lines highlighted
  463. * @see https://secure.php.net/highlight_string
  464. * @link https://book.cakephp.org/4/en/development/debugging.html#getting-an-excerpt-from-a-file
  465. */
  466. public static function excerpt(string $file, int $line, int $context = 2): array
  467. {
  468. $lines = [];
  469. if (!file_exists($file)) {
  470. return [];
  471. }
  472. $data = file_get_contents($file);
  473. if (empty($data)) {
  474. return $lines;
  475. }
  476. if (strpos($data, "\n") !== false) {
  477. $data = explode("\n", $data);
  478. }
  479. $line--;
  480. if (!isset($data[$line])) {
  481. return $lines;
  482. }
  483. for ($i = $line - $context; $i < $line + $context + 1; $i++) {
  484. if (!isset($data[$i])) {
  485. continue;
  486. }
  487. $string = str_replace(["\r\n", "\n"], '', static::_highlight($data[$i]));
  488. if ($i === $line) {
  489. $lines[] = '<span class="code-highlight">' . $string . '</span>';
  490. } else {
  491. $lines[] = $string;
  492. }
  493. }
  494. return $lines;
  495. }
  496. /**
  497. * Wraps the highlight_string function in case the server API does not
  498. * implement the function as it is the case of the HipHop interpreter
  499. *
  500. * @param string $str The string to convert.
  501. * @return string
  502. */
  503. protected static function _highlight(string $str): string
  504. {
  505. if (function_exists('hphp_log') || function_exists('hphp_gettid')) {
  506. return htmlentities($str);
  507. }
  508. $added = false;
  509. if (strpos($str, '<?php') === false) {
  510. $added = true;
  511. $str = "<?php \n" . $str;
  512. }
  513. $highlight = highlight_string($str, true);
  514. if ($added) {
  515. $highlight = str_replace(
  516. ['&lt;?php&nbsp;<br/>', '&lt;?php&nbsp;<br />'],
  517. '',
  518. $highlight
  519. );
  520. }
  521. return $highlight;
  522. }
  523. /**
  524. * Get the configured export formatter or infer one based on the environment.
  525. *
  526. * @return \Cake\Error\Debug\FormatterInterface
  527. * @unstable This method is not stable and may change in the future.
  528. * @since 4.1.0
  529. */
  530. public function getExportFormatter(): FormatterInterface
  531. {
  532. $instance = static::getInstance();
  533. $class = $instance->getConfig('exportFormatter');
  534. if (!$class) {
  535. if (ConsoleFormatter::environmentMatches()) {
  536. $class = ConsoleFormatter::class;
  537. } elseif (HtmlFormatter::environmentMatches()) {
  538. $class = HtmlFormatter::class;
  539. } else {
  540. $class = TextFormatter::class;
  541. }
  542. }
  543. $instance = new $class();
  544. if (!$instance instanceof FormatterInterface) {
  545. throw new RuntimeException(
  546. "The `{$class}` formatter does not implement " . FormatterInterface::class
  547. );
  548. }
  549. return $instance;
  550. }
  551. /**
  552. * Converts a variable to a string for debug output.
  553. *
  554. * *Note:* The following keys will have their contents
  555. * replaced with `*****`:
  556. *
  557. * - password
  558. * - login
  559. * - host
  560. * - database
  561. * - port
  562. * - prefix
  563. * - schema
  564. *
  565. * This is done to protect database credentials, which could be accidentally
  566. * shown in an error message if CakePHP is deployed in development mode.
  567. *
  568. * @param mixed $var Variable to convert.
  569. * @param int $maxDepth The depth to output to. Defaults to 3.
  570. * @return string Variable as a formatted string
  571. */
  572. public static function exportVar($var, int $maxDepth = 3): string
  573. {
  574. $context = new DebugContext($maxDepth);
  575. $node = static::export($var, $context);
  576. return static::getInstance()->getExportFormatter()->dump($node);
  577. }
  578. /**
  579. * Converts a variable to a plain text string.
  580. *
  581. * @param mixed $var Variable to convert.
  582. * @param int $maxDepth The depth to output to. Defaults to 3.
  583. * @return string Variable as a string
  584. */
  585. public static function exportVarAsPlainText($var, int $maxDepth = 3): string
  586. {
  587. return (new TextFormatter())->dump(
  588. static::export($var, new DebugContext($maxDepth))
  589. );
  590. }
  591. /**
  592. * Convert the variable to the internal node tree.
  593. *
  594. * The node tree can be manipulated and serialized more easily
  595. * than many object graphs can.
  596. *
  597. * @param mixed $var Variable to convert.
  598. * @param int $maxDepth The depth to generate nodes to. Defaults to 3.
  599. * @return \Cake\Error\Debug\NodeInterface The root node of the tree.
  600. */
  601. public static function exportVarAsNodes($var, int $maxDepth = 3): NodeInterface
  602. {
  603. return static::export($var, new DebugContext($maxDepth));
  604. }
  605. /**
  606. * Protected export function used to keep track of indentation and recursion.
  607. *
  608. * @param mixed $var The variable to dump.
  609. * @param \Cake\Error\Debug\DebugContext $context Dump context
  610. * @return \Cake\Error\Debug\NodeInterface The dumped variable.
  611. */
  612. protected static function export($var, DebugContext $context): NodeInterface
  613. {
  614. $type = static::getType($var);
  615. switch ($type) {
  616. case 'float':
  617. case 'string':
  618. case 'resource':
  619. case 'resource (closed)':
  620. case 'null':
  621. return new ScalarNode($type, $var);
  622. case 'boolean':
  623. return new ScalarNode('bool', $var);
  624. case 'integer':
  625. return new ScalarNode('int', $var);
  626. case 'array':
  627. return static::exportArray($var, $context->withAddedDepth());
  628. case 'unknown':
  629. return new SpecialNode('(unknown)');
  630. default:
  631. return static::exportObject($var, $context->withAddedDepth());
  632. }
  633. }
  634. /**
  635. * Export an array type object. Filters out keys used in datasource configuration.
  636. *
  637. * The following keys are replaced with ***'s
  638. *
  639. * - password
  640. * - login
  641. * - host
  642. * - database
  643. * - port
  644. * - prefix
  645. * - schema
  646. *
  647. * @param array $var The array to export.
  648. * @param \Cake\Error\Debug\DebugContext $context The current dump context.
  649. * @return \Cake\Error\Debug\ArrayNode Exported array.
  650. */
  651. protected static function exportArray(array $var, DebugContext $context): ArrayNode
  652. {
  653. $items = [];
  654. $remaining = $context->remainingDepth();
  655. if ($remaining >= 0) {
  656. $outputMask = static::outputMask();
  657. foreach ($var as $key => $val) {
  658. if (array_key_exists($key, $outputMask)) {
  659. $node = new ScalarNode('string', $outputMask[$key]);
  660. } elseif ($val !== $var) {
  661. // Dump all the items without increasing depth.
  662. $node = static::export($val, $context);
  663. } else {
  664. // Likely recursion, so we increase depth.
  665. $node = static::export($val, $context->withAddedDepth());
  666. }
  667. $items[] = new ArrayItemNode(static::export($key, $context), $node);
  668. }
  669. } else {
  670. $items[] = new ArrayItemNode(
  671. new ScalarNode('string', ''),
  672. new SpecialNode('[maximum depth reached]')
  673. );
  674. }
  675. return new ArrayNode($items);
  676. }
  677. /**
  678. * Handles object to node conversion.
  679. *
  680. * @param object $var Object to convert.
  681. * @param \Cake\Error\Debug\DebugContext $context The dump context.
  682. * @return \Cake\Error\Debug\NodeInterface
  683. * @see \Cake\Error\Debugger::exportVar()
  684. */
  685. protected static function exportObject(object $var, DebugContext $context): NodeInterface
  686. {
  687. $isRef = $context->hasReference($var);
  688. $refNum = $context->getReferenceId($var);
  689. $className = get_class($var);
  690. if ($isRef) {
  691. return new ReferenceNode($className, $refNum);
  692. }
  693. $node = new ClassNode($className, $refNum);
  694. $remaining = $context->remainingDepth();
  695. if ($remaining > 0) {
  696. if (method_exists($var, '__debugInfo')) {
  697. try {
  698. foreach ($var->__debugInfo() as $key => $val) {
  699. $node->addProperty(new PropertyNode("'{$key}'", null, static::export($val, $context)));
  700. }
  701. return $node;
  702. } catch (Exception $e) {
  703. return new SpecialNode("(unable to export object: {$e->getMessage()})");
  704. }
  705. }
  706. $outputMask = static::outputMask();
  707. $objectVars = get_object_vars($var);
  708. foreach ($objectVars as $key => $value) {
  709. if (array_key_exists($key, $outputMask)) {
  710. $value = $outputMask[$key];
  711. }
  712. /** @psalm-suppress RedundantCast */
  713. $node->addProperty(
  714. new PropertyNode((string)$key, 'public', static::export($value, $context->withAddedDepth()))
  715. );
  716. }
  717. $ref = new ReflectionObject($var);
  718. $filters = [
  719. ReflectionProperty::IS_PROTECTED => 'protected',
  720. ReflectionProperty::IS_PRIVATE => 'private',
  721. ];
  722. foreach ($filters as $filter => $visibility) {
  723. $reflectionProperties = $ref->getProperties($filter);
  724. foreach ($reflectionProperties as $reflectionProperty) {
  725. $reflectionProperty->setAccessible(true);
  726. if (
  727. method_exists($reflectionProperty, 'isInitialized') &&
  728. !$reflectionProperty->isInitialized($var)
  729. ) {
  730. $value = new SpecialNode('[uninitialized]');
  731. } else {
  732. $value = static::export($reflectionProperty->getValue($var), $context->withAddedDepth());
  733. }
  734. $node->addProperty(
  735. new PropertyNode(
  736. $reflectionProperty->getName(),
  737. $visibility,
  738. $value
  739. )
  740. );
  741. }
  742. }
  743. }
  744. return $node;
  745. }
  746. /**
  747. * Get the output format for Debugger error rendering.
  748. *
  749. * @return string Returns the current format when getting.
  750. */
  751. public static function getOutputFormat(): string
  752. {
  753. return Debugger::getInstance()->_outputFormat;
  754. }
  755. /**
  756. * Set the output format for Debugger error rendering.
  757. *
  758. * @param string $format The format you want errors to be output as.
  759. * @return void
  760. * @throws \InvalidArgumentException When choosing a format that doesn't exist.
  761. */
  762. public static function setOutputFormat(string $format): void
  763. {
  764. $self = Debugger::getInstance();
  765. if (!isset($self->_templates[$format])) {
  766. throw new InvalidArgumentException('Invalid Debugger output format.');
  767. }
  768. $self->_outputFormat = $format;
  769. }
  770. /**
  771. * Add an output format or update a format in Debugger.
  772. *
  773. * ```
  774. * Debugger::addFormat('custom', $data);
  775. * ```
  776. *
  777. * Where $data is an array of strings that use Text::insert() variable
  778. * replacement. The template vars should be in a `{:id}` style.
  779. * An error formatter can have the following keys:
  780. *
  781. * - 'error' - Used for the container for the error message. Gets the following template
  782. * variables: `id`, `error`, `code`, `description`, `path`, `line`, `links`, `info`
  783. * - 'info' - A combination of `code`, `context` and `trace`. Will be set with
  784. * the contents of the other template keys.
  785. * - 'trace' - The container for a stack trace. Gets the following template
  786. * variables: `trace`
  787. * - 'context' - The container element for the context variables.
  788. * Gets the following templates: `id`, `context`
  789. * - 'links' - An array of HTML links that are used for creating links to other resources.
  790. * Typically this is used to create javascript links to open other sections.
  791. * Link keys, are: `code`, `context`, `help`. See the JS output format for an
  792. * example.
  793. * - 'traceLine' - Used for creating lines in the stacktrace. Gets the following
  794. * template variables: `reference`, `path`, `line`
  795. *
  796. * Alternatively if you want to use a custom callback to do all the formatting, you can use
  797. * the callback key, and provide a callable:
  798. *
  799. * ```
  800. * Debugger::addFormat('custom', ['callback' => [$foo, 'outputError']];
  801. * ```
  802. *
  803. * The callback can expect two parameters. The first is an array of all
  804. * the error data. The second contains the formatted strings generated using
  805. * the other template strings. Keys like `info`, `links`, `code`, `context` and `trace`
  806. * will be present depending on the other templates in the format type.
  807. *
  808. * @param string $format Format to use, including 'js' for JavaScript-enhanced HTML, 'html' for
  809. * straight HTML output, or 'txt' for unformatted text.
  810. * @param array $strings Template strings, or a callback to be used for the output format.
  811. * @return array The resulting format string set.
  812. */
  813. public static function addFormat(string $format, array $strings): array
  814. {
  815. $self = Debugger::getInstance();
  816. if (isset($self->_templates[$format])) {
  817. if (isset($strings['links'])) {
  818. $self->_templates[$format]['links'] = array_merge(
  819. $self->_templates[$format]['links'],
  820. $strings['links']
  821. );
  822. unset($strings['links']);
  823. }
  824. $self->_templates[$format] = $strings + $self->_templates[$format];
  825. } else {
  826. $self->_templates[$format] = $strings;
  827. }
  828. return $self->_templates[$format];
  829. }
  830. /**
  831. * Takes a processed array of data from an error and displays it in the chosen format.
  832. *
  833. * @param array $data Data to output.
  834. * @return void
  835. */
  836. public function outputError(array $data): void
  837. {
  838. $defaults = [
  839. 'level' => 0,
  840. 'error' => 0,
  841. 'code' => 0,
  842. 'description' => '',
  843. 'file' => '',
  844. 'line' => 0,
  845. 'context' => [],
  846. 'start' => 2,
  847. ];
  848. $data += $defaults;
  849. $files = static::trace(['start' => $data['start'], 'format' => 'points']);
  850. $code = '';
  851. $file = null;
  852. if (isset($files[0]['file'])) {
  853. $file = $files[0];
  854. } elseif (isset($files[1]['file'])) {
  855. $file = $files[1];
  856. }
  857. if ($file) {
  858. $code = static::excerpt($file['file'], $file['line'], 1);
  859. }
  860. $trace = static::trace(['start' => $data['start'], 'depth' => '20']);
  861. $insertOpts = ['before' => '{:', 'after' => '}'];
  862. $context = [];
  863. $links = [];
  864. $info = '';
  865. foreach ((array)$data['context'] as $var => $value) {
  866. $context[] = "\${$var} = " . static::exportVar($value, 3);
  867. }
  868. switch ($this->_outputFormat) {
  869. case false:
  870. $this->_data[] = compact('context', 'trace') + $data;
  871. return;
  872. case 'log':
  873. static::log(compact('context', 'trace') + $data);
  874. return;
  875. }
  876. $data['trace'] = $trace;
  877. $data['id'] = 'cakeErr' . uniqid();
  878. $tpl = $this->_templates[$this->_outputFormat] + $this->_templates['base'];
  879. if (isset($tpl['links'])) {
  880. foreach ($tpl['links'] as $key => $val) {
  881. $links[$key] = Text::insert($val, $data, $insertOpts);
  882. }
  883. }
  884. if (!empty($tpl['escapeContext'])) {
  885. $data['description'] = h($data['description']);
  886. }
  887. $infoData = compact('code', 'context', 'trace');
  888. foreach ($infoData as $key => $value) {
  889. if (empty($value) || !isset($tpl[$key])) {
  890. continue;
  891. }
  892. if (is_array($value)) {
  893. $value = implode("\n", $value);
  894. }
  895. $info .= Text::insert($tpl[$key], [$key => $value] + $data, $insertOpts);
  896. }
  897. $links = implode(' ', $links);
  898. if (isset($tpl['callback']) && is_callable($tpl['callback'])) {
  899. $tpl['callback']($data, compact('links', 'info'));
  900. return;
  901. }
  902. echo Text::insert($tpl['error'], compact('links', 'info') + $data, $insertOpts);
  903. }
  904. /**
  905. * Get the type of the given variable. Will return the class name
  906. * for objects.
  907. *
  908. * @param mixed $var The variable to get the type of.
  909. * @return string The type of variable.
  910. */
  911. public static function getType($var): string
  912. {
  913. $type = getTypeName($var);
  914. if ($type === 'NULL') {
  915. return 'null';
  916. }
  917. if ($type === 'double') {
  918. return 'float';
  919. }
  920. if ($type === 'unknown type') {
  921. return 'unknown';
  922. }
  923. return $type;
  924. }
  925. /**
  926. * Prints out debug information about given variable.
  927. *
  928. * @param mixed $var Variable to show debug information for.
  929. * @param array $location If contains keys "file" and "line" their values will
  930. * be used to show location info.
  931. * @param bool|null $showHtml If set to true, the method prints the debug
  932. * data encoded as HTML. If false, plain text formatting will be used.
  933. * If null, the format will be chosen based on the configured exportFormatter, or
  934. * environment conditions.
  935. * @return void
  936. */
  937. public static function printVar($var, array $location = [], ?bool $showHtml = null): void
  938. {
  939. $location += ['file' => null, 'line' => null];
  940. if ($location['file']) {
  941. $location['file'] = static::trimPath((string)$location['file']);
  942. }
  943. $debugger = static::getInstance();
  944. $restore = null;
  945. if ($showHtml !== null) {
  946. $restore = $debugger->getConfig('exportFormatter');
  947. $debugger->setConfig('exportFormatter', $showHtml ? HtmlFormatter::class : TextFormatter::class);
  948. }
  949. $contents = static::exportVar($var, 25);
  950. $formatter = $debugger->getExportFormatter();
  951. if ($restore) {
  952. $debugger->setConfig('exportFormatter', $restore);
  953. }
  954. echo $formatter->formatWrapper($contents, $location);
  955. }
  956. /**
  957. * Format an exception message to be HTML formatted.
  958. *
  959. * Does the following formatting operations:
  960. *
  961. * - HTML escape the message.
  962. * - Convert `bool` into `<code>bool</code>`
  963. * - Convert newlines into `<br />`
  964. *
  965. * @param string $message The string message to format.
  966. * @return string Formatted message.
  967. */
  968. public static function formatHtmlMessage(string $message): string
  969. {
  970. $message = h($message);
  971. $message = preg_replace('/`([^`]+)`/', '<code>$1</code>', $message);
  972. $message = nl2br($message);
  973. return $message;
  974. }
  975. /**
  976. * Verifies that the application's salt and cipher seed value has been changed from the default value.
  977. *
  978. * @return void
  979. */
  980. public static function checkSecurityKeys(): void
  981. {
  982. $salt = Security::getSalt();
  983. if ($salt === '__SALT__' || strlen($salt) < 32) {
  984. trigger_error(
  985. 'Please change the value of `Security.salt` in `ROOT/config/app_local.php` ' .
  986. 'to a random value of at least 32 characters.',
  987. E_USER_NOTICE
  988. );
  989. }
  990. }
  991. }