errors.inc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <?php
  2. /**
  3. * @file
  4. * Functions for error handling.
  5. */
  6. use Drupal\Component\Utility\SafeMarkup;
  7. use Drupal\Component\Utility\Xss;
  8. use Drupal\Core\Logger\RfcLogLevel;
  9. use Drupal\Core\Render\Markup;
  10. use Drupal\Core\Utility\Error;
  11. use Symfony\Component\HttpFoundation\Response;
  12. /**
  13. * Maps PHP error constants to watchdog severity levels.
  14. *
  15. * The error constants are documented at
  16. * http://php.net/manual/errorfunc.constants.php
  17. *
  18. * @ingroup logging_severity_levels
  19. */
  20. function drupal_error_levels() {
  21. $types = array(
  22. E_ERROR => array('Error', RfcLogLevel::ERROR),
  23. E_WARNING => array('Warning', RfcLogLevel::WARNING),
  24. E_PARSE => array('Parse error', RfcLogLevel::ERROR),
  25. E_NOTICE => array('Notice', RfcLogLevel::NOTICE),
  26. E_CORE_ERROR => array('Core error', RfcLogLevel::ERROR),
  27. E_CORE_WARNING => array('Core warning', RfcLogLevel::WARNING),
  28. E_COMPILE_ERROR => array('Compile error', RfcLogLevel::ERROR),
  29. E_COMPILE_WARNING => array('Compile warning', RfcLogLevel::WARNING),
  30. E_USER_ERROR => array('User error', RfcLogLevel::ERROR),
  31. E_USER_WARNING => array('User warning', RfcLogLevel::WARNING),
  32. E_USER_NOTICE => array('User notice', RfcLogLevel::NOTICE),
  33. E_STRICT => array('Strict warning', RfcLogLevel::DEBUG),
  34. E_RECOVERABLE_ERROR => array('Recoverable fatal error', RfcLogLevel::ERROR),
  35. E_DEPRECATED => array('Deprecated function', RfcLogLevel::DEBUG),
  36. E_USER_DEPRECATED => array('User deprecated function', RfcLogLevel::DEBUG),
  37. );
  38. return $types;
  39. }
  40. /**
  41. * Provides custom PHP error handling.
  42. *
  43. * @param $error_level
  44. * The level of the error raised.
  45. * @param $message
  46. * The error message.
  47. * @param $filename
  48. * The filename that the error was raised in.
  49. * @param $line
  50. * The line number the error was raised at.
  51. * @param $context
  52. * An array that points to the active symbol table at the point the error
  53. * occurred.
  54. */
  55. function _drupal_error_handler_real($error_level, $message, $filename, $line, $context) {
  56. if ($error_level & error_reporting()) {
  57. $types = drupal_error_levels();
  58. list($severity_msg, $severity_level) = $types[$error_level];
  59. $backtrace = debug_backtrace();
  60. $caller = Error::getLastCaller($backtrace);
  61. // We treat recoverable errors as fatal.
  62. $recoverable = $error_level == E_RECOVERABLE_ERROR;
  63. // As __toString() methods must not throw exceptions (recoverable errors)
  64. // in PHP, we allow them to trigger a fatal error by emitting a user error
  65. // using trigger_error().
  66. $to_string = $error_level == E_USER_ERROR && substr($caller['function'], -strlen('__toString()')) == '__toString()';
  67. _drupal_log_error(array(
  68. '%type' => isset($types[$error_level]) ? $severity_msg : 'Unknown error',
  69. // The standard PHP error handler considers that the error messages
  70. // are HTML. We mimick this behavior here.
  71. '@message' => Markup::create(Xss::filterAdmin($message)),
  72. '%function' => $caller['function'],
  73. '%file' => $caller['file'],
  74. '%line' => $caller['line'],
  75. 'severity_level' => $severity_level,
  76. 'backtrace' => $backtrace,
  77. ), $recoverable || $to_string);
  78. }
  79. }
  80. /**
  81. * Determines whether an error should be displayed.
  82. *
  83. * When in maintenance mode or when error_level is ERROR_REPORTING_DISPLAY_ALL,
  84. * all errors should be displayed. For ERROR_REPORTING_DISPLAY_SOME, $error
  85. * will be examined to determine if it should be displayed.
  86. *
  87. * @param $error
  88. * Optional error to examine for ERROR_REPORTING_DISPLAY_SOME.
  89. *
  90. * @return
  91. * TRUE if an error should be displayed.
  92. */
  93. function error_displayable($error = NULL) {
  94. if (defined('MAINTENANCE_MODE')) {
  95. return TRUE;
  96. }
  97. $error_level = _drupal_get_error_level();
  98. if ($error_level == ERROR_REPORTING_DISPLAY_ALL || $error_level == ERROR_REPORTING_DISPLAY_VERBOSE) {
  99. return TRUE;
  100. }
  101. if ($error_level == ERROR_REPORTING_DISPLAY_SOME && isset($error)) {
  102. return $error['%type'] != 'Notice' && $error['%type'] != 'Strict warning';
  103. }
  104. return FALSE;
  105. }
  106. /**
  107. * Logs a PHP error or exception and displays an error page in fatal cases.
  108. *
  109. * @param $error
  110. * An array with the following keys: %type, @message, %function, %file,
  111. * %line, severity_level, and backtrace. All the parameters are plain-text,
  112. * with the exception of @message, which needs to be an HTML string, and
  113. * backtrace, which is a standard PHP backtrace.
  114. * @param bool $fatal
  115. * TRUE for:
  116. * - An exception is thrown and not caught by something else.
  117. * - A recoverable fatal error, which is a fatal error.
  118. * Non-recoverable fatal errors cannot be logged by Drupal.
  119. */
  120. function _drupal_log_error($error, $fatal = FALSE) {
  121. $is_installer = drupal_installation_attempted();
  122. // Backtrace array is not a valid replacement value for t().
  123. $backtrace = $error['backtrace'];
  124. unset($error['backtrace']);
  125. // When running inside the testing framework, we relay the errors
  126. // to the tested site by the way of HTTP headers.
  127. if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) {
  128. // $number does not use drupal_static as it should not be reset
  129. // as it uniquely identifies each PHP error.
  130. static $number = 0;
  131. $assertion = array(
  132. $error['@message'],
  133. $error['%type'],
  134. array(
  135. 'function' => $error['%function'],
  136. 'file' => $error['%file'],
  137. 'line' => $error['%line'],
  138. ),
  139. );
  140. // For non-fatal errors (e.g. PHP notices) _drupal_log_error can be called
  141. // multiple times per request. In that case the response is typically
  142. // generated outside of the error handler, e.g., in a controller. As a
  143. // result it is not possible to use a Response object here but instead the
  144. // headers need to be emitted directly.
  145. header('X-Drupal-Assertion-' . $number . ': ' . rawurlencode(serialize($assertion)));
  146. $number++;
  147. }
  148. $response = new Response();
  149. // Only call the logger if there is a logger factory available. This can occur
  150. // if there is an error while rebuilding the container or during the
  151. // installer.
  152. if (\Drupal::hasService('logger.factory')) {
  153. try {
  154. \Drupal::logger('php')->log($error['severity_level'], '%type: @message in %function (line %line of %file).', $error);
  155. }
  156. catch (\Exception $e) {
  157. // We can't log, for example because the database connection is not
  158. // available. At least try to log to PHP error log.
  159. error_log(strtr('Failed to log error: %type: @message in %function (line %line of %file).', $error));
  160. }
  161. }
  162. // Log fatal errors, so developers can find and debug them.
  163. if ($fatal) {
  164. error_log(sprintf('%s: %s in %s on line %d', $error['%type'], $error['@message'], $error['%file'], $error['%line']));
  165. }
  166. if (PHP_SAPI === 'cli') {
  167. if ($fatal) {
  168. // When called from CLI, simply output a plain text message.
  169. // Should not translate the string to avoid errors producing more errors.
  170. $response->setContent(html_entity_decode(strip_tags(SafeMarkup::format('%type: @message in %function (line %line of %file).', $error))) . "\n");
  171. $response->send();
  172. exit;
  173. }
  174. }
  175. if (\Drupal::hasRequest() && \Drupal::request()->isXmlHttpRequest()) {
  176. if ($fatal) {
  177. if (error_displayable($error)) {
  178. // When called from JavaScript, simply output the error message.
  179. // Should not translate the string to avoid errors producing more errors.
  180. $response->setContent(SafeMarkup::format('%type: @message in %function (line %line of %file).', $error));
  181. $response->send();
  182. }
  183. exit;
  184. }
  185. }
  186. else {
  187. // Display the message if the current error reporting level allows this type
  188. // of message to be displayed, and unconditionally in update.php.
  189. $message = '';
  190. $class = NULL;
  191. if (error_displayable($error)) {
  192. $class = 'error';
  193. // If error type is 'User notice' then treat it as debug information
  194. // instead of an error message.
  195. // @see debug()
  196. if ($error['%type'] == 'User notice') {
  197. $error['%type'] = 'Debug';
  198. $class = 'status';
  199. }
  200. // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
  201. // in the message. This does not happen for (false) security.
  202. if (\Drupal::hasService('app.root')) {
  203. $root_length = strlen(\Drupal::root());
  204. if (substr($error['%file'], 0, $root_length) == \Drupal::root()) {
  205. $error['%file'] = substr($error['%file'], $root_length + 1);
  206. }
  207. }
  208. // Check if verbose error reporting is on.
  209. $error_level = _drupal_get_error_level();
  210. if ($error_level != ERROR_REPORTING_DISPLAY_VERBOSE) {
  211. // Without verbose logging, use a simple message.
  212. // We call SafeMarkup::format() directly here, rather than use t() since
  213. // we are in the middle of error handling, and we don't want t() to
  214. // cause further errors.
  215. $message = SafeMarkup::format('%type: @message in %function (line %line of %file).', $error);
  216. }
  217. else {
  218. // With verbose logging, we will also include a backtrace.
  219. // First trace is the error itself, already contained in the message.
  220. // While the second trace is the error source and also contained in the
  221. // message, the message doesn't contain argument values, so we output it
  222. // once more in the backtrace.
  223. array_shift($backtrace);
  224. // Generate a backtrace containing only scalar argument values.
  225. $error['@backtrace'] = Error::formatBacktrace($backtrace);
  226. $message = SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $error);
  227. }
  228. }
  229. if ($fatal) {
  230. // We fallback to a maintenance page at this point, because the page generation
  231. // itself can generate errors.
  232. // Should not translate the string to avoid errors producing more errors.
  233. $message = 'The website encountered an unexpected error. Please try again later.' . '<br />' . $message;
  234. if ($is_installer) {
  235. // install_display_output() prints the output and ends script execution.
  236. $output = array(
  237. '#title' => 'Error',
  238. '#markup' => $message,
  239. );
  240. install_display_output($output, $GLOBALS['install_state'], $response->headers->all());
  241. exit;
  242. }
  243. $response->setContent($message);
  244. $response->setStatusCode(500, '500 Service unavailable (with message)');
  245. $response->send();
  246. // An exception must halt script execution.
  247. exit;
  248. }
  249. if ($message) {
  250. if (\Drupal::hasService('session')) {
  251. // Message display is dependent on sessions being available.
  252. drupal_set_message($message, $class, TRUE);
  253. }
  254. else {
  255. print $message;
  256. }
  257. }
  258. }
  259. }
  260. /**
  261. * Returns the current error level.
  262. *
  263. * This function should only be used to get the current error level prior to the
  264. * kernel being booted or before Drupal is installed. In all other situations
  265. * the following code is preferred:
  266. * @code
  267. * \Drupal::config('system.logging')->get('error_level');
  268. * @endcode
  269. *
  270. * @return string
  271. * The current error level.
  272. */
  273. function _drupal_get_error_level() {
  274. // Raise the error level to maximum for the installer, so users are able to
  275. // file proper bug reports for installer errors. The returned value is
  276. // different to the one below, because the installer actually has a
  277. // 'config.factory' service, which reads the default 'error_level' value from
  278. // System module's default configuration and the default value is not verbose.
  279. // @see error_displayable()
  280. if (drupal_installation_attempted()) {
  281. return ERROR_REPORTING_DISPLAY_VERBOSE;
  282. }
  283. $error_level = NULL;
  284. // Try to get the error level configuration from database. If this fails,
  285. // for example if the database connection is not there, try to read it from
  286. // settings.php.
  287. try {
  288. $error_level = \Drupal::config('system.logging')->get('error_level');
  289. }
  290. catch (\Exception $e) {
  291. $error_level = isset($GLOBALS['config']['system.logging']['error_level']) ? $GLOBALS['config']['system.logging']['error_level'] : ERROR_REPORTING_HIDE;
  292. }
  293. // If there is no container or if it has no config.factory service, we are
  294. // possibly in an edge-case error situation while trying to serve a regular
  295. // request on a public site, so use the non-verbose default value.
  296. return $error_level ?: ERROR_REPORTING_DISPLAY_ALL;
  297. }