Error.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. <?php
  2. namespace Drupal\Core\Utility;
  3. use Drupal\Component\Render\FormattableMarkup;
  4. use Drupal\Component\Utility\Xss;
  5. use Drupal\Core\Database\DatabaseExceptionWrapper;
  6. /**
  7. * Drupal error utility class.
  8. */
  9. class Error {
  10. /**
  11. * The error severity level.
  12. *
  13. * @var int
  14. */
  15. const ERROR = 3;
  16. /**
  17. * An array of blacklisted functions.
  18. *
  19. * @var array
  20. */
  21. protected static $blacklistFunctions = ['debug', '_drupal_error_handler', '_drupal_exception_handler'];
  22. /**
  23. * Decodes an exception and retrieves the correct caller.
  24. *
  25. * @param \Exception|\Throwable $exception
  26. * The exception object that was thrown.
  27. *
  28. * @return array
  29. * An error in the format expected by _drupal_log_error().
  30. */
  31. public static function decodeException($exception) {
  32. $message = $exception->getMessage();
  33. $backtrace = $exception->getTrace();
  34. // Add the line throwing the exception to the backtrace.
  35. array_unshift($backtrace, ['line' => $exception->getLine(), 'file' => $exception->getFile()]);
  36. // For PDOException errors, we try to return the initial caller,
  37. // skipping internal functions of the database layer.
  38. if ($exception instanceof \PDOException || $exception instanceof DatabaseExceptionWrapper) {
  39. // The first element in the stack is the call, the second element gives us
  40. // the caller. We skip calls that occurred in one of the classes of the
  41. // database layer or in one of its global functions.
  42. $db_functions = ['db_query', 'db_query_range'];
  43. while (!empty($backtrace[1]) && ($caller = $backtrace[1]) &&
  44. ((isset($caller['class']) && (strpos($caller['class'], 'Query') !== FALSE || strpos($caller['class'], 'Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE)) ||
  45. in_array($caller['function'], $db_functions))) {
  46. // We remove that call.
  47. array_shift($backtrace);
  48. }
  49. if (isset($exception->query_string, $exception->args)) {
  50. $message .= ": " . $exception->query_string . "; " . print_r($exception->args, TRUE);
  51. }
  52. }
  53. $caller = static::getLastCaller($backtrace);
  54. return [
  55. '%type' => get_class($exception),
  56. // The standard PHP exception handler considers that the exception message
  57. // is plain-text. We mimic this behavior here.
  58. '@message' => $message,
  59. '%function' => $caller['function'],
  60. '%file' => $caller['file'],
  61. '%line' => $caller['line'],
  62. 'severity_level' => static::ERROR,
  63. 'backtrace' => $backtrace,
  64. '@backtrace_string' => $exception->getTraceAsString(),
  65. ];
  66. }
  67. /**
  68. * Renders an exception error message without further exceptions.
  69. *
  70. * @param \Exception|\Throwable $exception
  71. * The exception object that was thrown.
  72. *
  73. * @return string
  74. * An error message.
  75. */
  76. public static function renderExceptionSafe($exception) {
  77. $decode = static::decodeException($exception);
  78. $backtrace = $decode['backtrace'];
  79. unset($decode['backtrace']);
  80. // Remove 'main()'.
  81. array_shift($backtrace);
  82. // Even though it is possible that this method is called on a public-facing
  83. // site, it is only called when the exception handler itself threw an
  84. // exception, which normally means that a code change caused the system to
  85. // no longer function correctly (as opposed to a user-triggered error), so
  86. // we assume that it is safe to include a verbose backtrace.
  87. $decode['@backtrace'] = Error::formatBacktrace($backtrace);
  88. return new FormattableMarkup('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $decode);
  89. }
  90. /**
  91. * Gets the last caller from a backtrace.
  92. *
  93. * @param array $backtrace
  94. * A standard PHP backtrace. Passed by reference.
  95. *
  96. * @return array
  97. * An associative array with keys 'file', 'line' and 'function'.
  98. */
  99. public static function getLastCaller(array &$backtrace) {
  100. // Errors that occur inside PHP internal functions do not generate
  101. // information about file and line. Ignore black listed functions.
  102. while (($backtrace && !isset($backtrace[0]['line'])) ||
  103. (isset($backtrace[1]['function']) && in_array($backtrace[1]['function'], static::$blacklistFunctions))) {
  104. array_shift($backtrace);
  105. }
  106. // The first trace is the call itself.
  107. // It gives us the line and the file of the last call.
  108. $call = $backtrace[0];
  109. // The second call gives us the function where the call originated.
  110. if (isset($backtrace[1])) {
  111. if (isset($backtrace[1]['class'])) {
  112. $call['function'] = $backtrace[1]['class'] . $backtrace[1]['type'] . $backtrace[1]['function'] . '()';
  113. }
  114. else {
  115. $call['function'] = $backtrace[1]['function'] . '()';
  116. }
  117. }
  118. else {
  119. $call['function'] = 'main()';
  120. }
  121. return $call;
  122. }
  123. /**
  124. * Formats a backtrace into a plain-text string.
  125. *
  126. * The calls show values for scalar arguments and type names for complex ones.
  127. *
  128. * @param array $backtrace
  129. * A standard PHP backtrace.
  130. *
  131. * @return string
  132. * A plain-text line-wrapped string ready to be put inside <pre>.
  133. */
  134. public static function formatBacktrace(array $backtrace) {
  135. $return = '';
  136. foreach ($backtrace as $trace) {
  137. $call = ['function' => '', 'args' => []];
  138. if (isset($trace['class'])) {
  139. $call['function'] = $trace['class'] . $trace['type'] . $trace['function'];
  140. }
  141. elseif (isset($trace['function'])) {
  142. $call['function'] = $trace['function'];
  143. }
  144. else {
  145. $call['function'] = 'main';
  146. }
  147. if (isset($trace['args'])) {
  148. foreach ($trace['args'] as $arg) {
  149. if (is_scalar($arg)) {
  150. $call['args'][] = is_string($arg) ? '\'' . Xss::filter($arg) . '\'' : $arg;
  151. }
  152. else {
  153. $call['args'][] = ucfirst(gettype($arg));
  154. }
  155. }
  156. }
  157. $line = '';
  158. if (isset($trace['line'])) {
  159. $line = " (Line: {$trace['line']})";
  160. }
  161. $return .= $call['function'] . '(' . implode(', ', $call['args']) . ")$line\n";
  162. }
  163. return $return;
  164. }
  165. }