errors.inc 14 KB

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