/** * Log not-otherwise-specified errors, including HTTP 500. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event * The event to process. */ public function onError(GetResponseForExceptionEvent $event) { $exception = $event->getException(); $error = Error::decodeException($exception); $this->logger->get('php')->log($error['severity_level'], '%type: @message in %function (line %line of %file).', $error); $is_critical = !$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500; if ($is_critical) { error_log(sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', get_class($exception), $exception->getMessage(), $exception->getFile(), $exception->getLine())); } }
/** * Renders an exception error message without further exceptions. * * @param \Exception|\Throwable $exception * The exception object that was thrown. * * @return string * An error message. */ public static function renderExceptionSafe($exception) { $decode = static::decodeException($exception); $backtrace = $decode['backtrace']; unset($decode['backtrace']); // Remove 'main()'. array_shift($backtrace); // Even though it is possible that this method is called on a public-facing // site, it is only called when the exception handler itself threw an // exception, which normally means that a code change caused the system to // no longer function correctly (as opposed to a user-triggered error), so // we assume that it is safe to include a verbose backtrace. $decode['@backtrace'] = Error::formatBacktrace($backtrace); return SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $decode); }
/** * Checks for special handling of errors inside Simpletest. * * @todo The $headers array appears to not actually get used at all in the * original code. It's quite possible that this entire method is now * vestigial and can be removed. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event */ public function on500(GetResponseForExceptionEvent $event) { $exception = $event->getException(); $error = Error::decodeException($exception); $headers = array(); // When running inside the testing framework, we relay the errors // to the tested site by the way of HTTP headers. if (DRUPAL_TEST_IN_CHILD_SITE && !headers_sent() && (!defined('SIMPLETEST_COLLECT_ERRORS') || SIMPLETEST_COLLECT_ERRORS)) { // $number does not use drupal_static as it should not be reset // as it uniquely identifies each PHP error. static $number = 0; $assertion = array($error['@message'], $error['%type'], array('function' => $error['%function'], 'file' => $error['%file'], 'line' => $error['%line'])); $headers['X-Drupal-Assertion-' . $number] = rawurlencode(serialize($assertion)); $number++; } }
/** * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output) { $io = new DrupalStyle($input, $output); $state = $this->getService('state'); $io->info($this->trans('commands.site.maintenance.messages.maintenance-on')); $io->info($this->trans('commands.update.entities.messages.start')); $state->set('system.maintenance_mode', true); try { $this->getService('entity.definition_update_manager')->applyUpdates(); } catch (EntityStorageException $e) { $variables = Error::decodeException($e); $io->info($this->trans('commands.update.entities.messages.error')); $io->info($variables); } $state->set('system.maintenance_mode', false); $io->info($this->trans('commands.update.entities.messages.end')); $this->getChain()->addCommand('cache:rebuild', ['cache' => 'all']); $io->info($this->trans('commands.site.maintenance.messages.maintenance-off')); }
/** * {@inheritdoc} */ public function write($sid, $value) { // The exception handler is not active at this point, so we need to do it // manually. try { $request = $this->requestStack->getCurrentRequest(); $fields = array('uid' => $request->getSession()->get('uid', 0), 'hostname' => $request->getClientIP(), 'session' => $value, 'timestamp' => REQUEST_TIME); $this->connection->merge('sessions')->keys(array('sid' => Crypt::hashBase64($sid)))->fields($fields)->execute(); return TRUE; } catch (\Exception $exception) { require_once DRUPAL_ROOT . '/core/includes/errors.inc'; // If we are displaying errors, then do so with no possibility of a // further uncaught exception being thrown. if (error_displayable()) { print '<h1>Uncaught exception thrown in session handler.</h1>'; print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />'; } return FALSE; } }
/** * {@inheritdoc} * * HTTP middleware that replaces the user agent for simpletest requests. */ public function __invoke() { // If the database prefix is being used by SimpleTest to run the tests in a copied // database then set the user-agent header to the database prefix so that any // calls to other Drupal pages will run the SimpleTest prefixed database. The // user-agent is used to ensure that multiple testing sessions running at the // same time won't interfere with each other as they would if the database // prefix were stored statically in a file or database variable. return function ($handler) { return function (RequestInterface $request, array $options) use($handler) { if ($test_prefix = drupal_valid_test_ua()) { $request = $request->withHeader('User-Agent', drupal_generate_test_ua($test_prefix)); } return $handler($request, $options)->then(function (ResponseInterface $response) use($request) { if (!drupal_valid_test_ua()) { return $response; } $headers = $response->getHeaders(); foreach ($headers as $header_name => $header_values) { if (preg_match('/^X-Drupal-Assertion-[0-9]+$/', $header_name, $matches)) { foreach ($header_values as $header_value) { // Call \Drupal\simpletest\WebTestBase::error() with the parameters from // the header. $parameters = unserialize(urldecode($header_value)); if (count($parameters) === 3) { throw new \Exception($parameters[1] . ': ' . $parameters[0] . "\n" . Error::formatBacktrace([$parameters[2]])); } else { throw new \Exception('Error thrown with the wrong amount of parameters.'); } } } } return $response; }); }; }; }
/** * Takes an Exception object and both saves and displays it. * * Pulls in additional information on the location triggering the exception. * * @param \Exception $exception * Object representing the exception. * @param bool $save * (optional) Whether to save the message in the migration's mapping table. * Set to FALSE in contexts where this doesn't make sense. */ protected function handleException(\Exception $exception, $save = TRUE) { $result = Error::decodeException($exception); $message = $result['@message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')'; if ($save) { $this->saveMessage($message); } $this->message->display($message, 'error'); }
/** * This method is a temporary port of _drupal_decode_exception(). * * @todo This should get refactored. FlattenException could use some * improvement as well. * * @param \Symfony\Component\Debug\Exception\FlattenException $exception * The flattened exception. * * @return array * An array of string-substitution tokens for formatting a message about the * exception. */ protected function decodeException(FlattenException $exception) { $message = $exception->getMessage(); $backtrace = $exception->getTrace(); // This value is missing from the stack for some reason in the // FlattenException version of the backtrace. $backtrace[0]['line'] = $exception->getLine(); // For database errors, we try to return the initial caller, // skipping internal functions of the database layer. if (strpos($exception->getClass(), 'DatabaseExceptionWrapper') !== FALSE) { // A DatabaseExceptionWrapper exception is actually just a courier for // the original PDOException. It's the stack trace from that exception // that we care about. $backtrace = $exception->getPrevious()->getTrace(); $backtrace[0]['line'] = $exception->getLine(); // The first element in the stack is the call, the second element gives us the caller. // We skip calls that occurred in one of the classes of the database layer // or in one of its global functions. $db_functions = array('db_query', 'db_query_range'); while (!empty($backtrace[1]) && ($caller = $backtrace[1]) && (strpos($caller['namespace'], 'Drupal\\Core\\Database') !== FALSE || strpos($caller['class'], 'PDO') !== FALSE) || in_array($caller['function'], $db_functions)) { // We remove that call. array_shift($backtrace); } } $caller = Error::getLastCaller($backtrace); return array('%type' => $exception->getClass(), '!message' => String::checkPlain($message), '%function' => $caller['function'], '%file' => $caller['file'], '%line' => $caller['line'], 'severity_level' => WATCHDOG_ERROR); }
/** * Handle exceptions. * * @see set_exception_handler */ protected function exceptionHandler($exception) { $backtrace = $exception->getTrace(); $verbose_backtrace = $backtrace; // Push on top of the backtrace the call that generated the exception. array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile())); $decoded_exception = Error::decodeException($exception); unset($decoded_exception['backtrace']); $message = SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $decoded_exception + array('@backtrace' => Error::formatBacktrace($verbose_backtrace))); $this->error($message, 'Uncaught exception', Error::getLastCaller($backtrace)); }
/** * {@inheritdoc} */ public function write($sid, $value) { global $user; // The exception handler is not active at this point, so we need to do it // manually. try { if (!$this->sessionManager->isEnabled()) { // We don't have anything to do if we are not allowed to save the // session. return TRUE; } // Either ssid or sid or both will be added from $key below. $fields = array('uid' => $user->id(), 'hostname' => $this->requestStack->getCurrentRequest()->getClientIP(), 'session' => $value, 'timestamp' => REQUEST_TIME); // Use the session ID as 'sid' and an empty string as 'ssid' by default. // read() does not allow empty strings so that's a safe default. $key = array('sid' => Crypt::hashBase64($sid), 'ssid' => ''); // On HTTPS connections, use the session ID as both 'sid' and 'ssid'. if ($this->requestStack->getCurrentRequest()->isSecure()) { $key['ssid'] = $key['sid']; // The "secure pages" setting allows a site to simultaneously use both // secure and insecure session cookies. If enabled and both cookies // are presented then use both keys. The session ID from the cookie is // hashed before being stored in the database as a security measure. if ($this->sessionManager->isMixedMode()) { $insecure_session_name = $this->sessionManager->getInsecureName(); $cookies = $this->requestStack->getCurrentRequest()->cookies; if ($cookies->has($insecure_session_name)) { $key['sid'] = Crypt::hashBase64($cookies->get($insecure_session_name)); } } } elseif ($this->sessionManager->isMixedMode()) { unset($key['ssid']); } $this->connection->merge('sessions')->keys($key)->fields($fields)->execute(); // Remove obsolete sessions. $this->cleanupObsoleteSessions(); // Likewise, do not update access time more than once per 180 seconds. if ($user->isAuthenticated() && REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) { /** @var \Drupal\user\UserStorageInterface $storage */ $storage = \Drupal::entityManager()->getStorage('user'); $storage->updateLastAccessTimestamp($user, REQUEST_TIME); } return TRUE; } catch (\Exception $exception) { require_once DRUPAL_ROOT . '/core/includes/errors.inc'; // If we are displaying errors, then do so with no possibility of a // further uncaught exception being thrown. if (error_displayable()) { print '<h1>Uncaught exception thrown in session handler.</h1>'; print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />'; } return FALSE; } }
/** * Makes a subrequest to retrieve the default error page. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event * The event to process * @param string $url * The path/url to which to make a subrequest for this error message. * @param int $status_code * The status code for the error being handled. */ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $status_code) { $request = $event->getRequest(); $exception = $event->getException(); if (!($url && $url[0] == '/')) { $url = $request->getBasePath() . '/' . $url; } $current_url = $request->getBasePath() . $request->getPathInfo(); if ($url != $request->getBasePath() . '/' && $url != $current_url) { if ($request->getMethod() === 'POST') { $sub_request = Request::create($url, 'POST', $this->redirectDestination->getAsArray() + ['_exception_statuscode' => $status_code] + $request->request->all(), $request->cookies->all(), [], $request->server->all()); } else { $sub_request = Request::create($url, 'GET', $request->query->all() + $this->redirectDestination->getAsArray() + ['_exception_statuscode' => $status_code], $request->cookies->all(), [], $request->server->all()); } try { // Persist the 'exception' attribute to the subrequest. $sub_request->attributes->set('exception', $request->attributes->get('exception')); // Persist the access result attribute to the subrequest, so that the // error page inherits the access result of the master request. $sub_request->attributes->set(AccessAwareRouterInterface::ACCESS_RESULT, $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT)); // Carry over the session to the subrequest. if ($session = $request->getSession()) { $sub_request->setSession($session); } $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST); // Only 2xx responses should have their status code overridden; any // other status code should be passed on: redirects (3xx), error (5xx)… // @see https://www.drupal.org/node/2603788#comment-10504916 if ($response->isSuccessful()) { $response->setStatusCode($status_code); } // Persist any special HTTP headers that were set on the exception. if ($exception instanceof HttpExceptionInterface) { $response->headers->add($exception->getHeaders()); } $event->setResponse($response); } catch (\Exception $e) { // If an error happened in the subrequest we can't do much else. Instead, // just log it. The DefaultExceptionSubscriber will catch the original // exception and handle it normally. $error = Error::decodeException($e); $this->logger->log($error['severity_level'], '%type: @message in %function (line %line of %file).', $error); } } }
/** * Makes a subrequest to retrieve the default error page. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event * The event to process. * @param string $url * The path/url to which to make a subrequest for this error message. * @param int $status_code * The status code for the error being handled. */ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $status_code) { $request = $event->getRequest(); $exception = $event->getException(); try { // Reuse the exact same request (so keep the same URL, keep the access // result, the exception, et cetera) but override the routing information. // This means that aside from routing, this is identical to the master // request. This allows us to generate a response that is executed on // behalf of the master request, i.e. for the original URL. This is what // allows us to e.g. generate a 404 response for the original URL; if we // would execute a subrequest with the 404 route's URL, then it'd be // generated for *that* URL, not the *original* URL. $sub_request = clone $request; $sub_request->attributes->add($this->accessUnawareRouter->match($url)); // Add to query (GET) or request (POST) parameters: // - 'destination' (to ensure e.g. the login form in a 403 response // redirects to the original URL) // - '_exception_statuscode' $parameters = $sub_request->isMethod('GET') ? $sub_request->query : $sub_request->request; $parameters->add($this->redirectDestination->getAsArray() + ['_exception_statuscode' => $status_code]); $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST); // Only 2xx responses should have their status code overridden; any // other status code should be passed on: redirects (3xx), error (5xx)… // @see https://www.drupal.org/node/2603788#comment-10504916 if ($response->isSuccessful()) { $response->setStatusCode($status_code); } // Persist any special HTTP headers that were set on the exception. if ($exception instanceof HttpExceptionInterface) { $response->headers->add($exception->getHeaders()); } $event->setResponse($response); } catch (\Exception $e) { // If an error happened in the subrequest we can't do much else. Instead, // just log it. The DefaultExceptionSubscriber will catch the original // exception and handle it normally. $error = Error::decodeException($e); $this->logger->log($error['severity_level'], '%type: @message in %function (line %line of %file).', $error); } }
/** * {@inheritdoc} */ public function write($sid, $value) { $user = \Drupal::currentUser(); // The exception handler is not active at this point, so we need to do it // manually. try { $fields = array('uid' => $user->id(), 'hostname' => $this->requestStack->getCurrentRequest()->getClientIP(), 'session' => $value, 'timestamp' => REQUEST_TIME); $this->connection->merge('sessions')->keys(array('sid' => Crypt::hashBase64($sid)))->fields($fields)->execute(); // Likewise, do not update access time more than once per 180 seconds. if ($user->isAuthenticated() && REQUEST_TIME - $user->getLastAccessedTime() > Settings::get('session_write_interval', 180)) { /** @var \Drupal\user\UserStorageInterface $storage */ $storage = \Drupal::entityManager()->getStorage('user'); $storage->updateLastAccessTimestamp($user, REQUEST_TIME); } return TRUE; } catch (\Exception $exception) { require_once DRUPAL_ROOT . '/core/includes/errors.inc'; // If we are displaying errors, then do so with no possibility of a // further uncaught exception being thrown. if (error_displayable()) { print '<h1>Uncaught exception thrown in session handler.</h1>'; print '<p>' . Error::renderExceptionSafe($exception) . '</p><hr />'; } return FALSE; } }
/** * Handle exceptions. * * @see set_exception_handler */ protected function exceptionHandler($exception) { require_once DRUPAL_ROOT . '/core/includes/errors.inc'; $backtrace = $exception->getTrace(); $verbose_backtrace = $backtrace; // Push on top of the backtrace the call that generated the exception. array_unshift($backtrace, array('line' => $exception->getLine(), 'file' => $exception->getFile())); // \Drupal\Core\Utility\Error::decodeException() runs the exception // message through \Drupal\Component\Utility\String::checkPlain(). $decoded_exception = Error::decodeException($exception); unset($decoded_exception['backtrace']); $message = String::format('%type: !message in %function (line %line of %file). <pre class="backtrace">!backtrace</pre>', $decoded_exception + array('!backtrace' => Error::formatBacktrace($verbose_backtrace))); $this->error($message, 'Uncaught exception', Error::getLastCaller($backtrace)); }
/** * Makes a subrequest to retrieve the default error page. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event * The event to process * @param string $path * The path to which to make a subrequest for this error message. * @param int $status_code * The status code for the error being handled. */ protected function makeSubrequest(GetResponseForExceptionEvent $event, $path, $status_code) { $request = $event->getRequest(); // @todo Remove dependency on the internal _system_path attribute: // https://www.drupal.org/node/2293523. $system_path = $request->attributes->get('_system_path'); if ($path && $path != $system_path) { if ($request->getMethod() === 'POST') { $sub_request = Request::create($request->getBaseUrl() . '/' . $path, 'POST', ['destination' => $system_path, '_exception_statuscode' => $status_code] + $request->request->all(), $request->cookies->all(), [], $request->server->all()); } else { $sub_request = Request::create($request->getBaseUrl() . '/' . $path, 'GET', $request->query->all() + ['destination' => $system_path, '_exception_statuscode' => $status_code], $request->cookies->all(), [], $request->server->all()); } try { // Persist the 'exception' attribute to the subrequest. $sub_request->attributes->set('exception', $request->attributes->get('exception')); $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST); $response->setStatusCode($status_code); $event->setResponse($response); } catch (\Exception $e) { // If an error happened in the subrequest we can't do much else. Instead, // just log it. The DefaultExceptionSubscriber will catch the original // exception and handle it normally. $error = Error::decodeException($e); $this->logger->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error); } } }
/** * Handles any exception as a generic error page for JSON. * * @todo This should probably check the error reporting level. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event * The event to process. */ protected function onJson(GetResponseForExceptionEvent $event) { $exception = $event->getException(); $error = Error::decodeException($exception); // Display the message if the current error reporting level allows this type // of message to be displayed, $data = NULL; if (error_displayable($error) && ($message = $exception->getMessage())) { $data = ['message' => sprintf('A fatal error occurred: %s', $message)]; } $response = new JsonResponse($data, Response::HTTP_INTERNAL_SERVER_ERROR); if ($exception instanceof HttpExceptionInterface) { $response->setStatusCode($exception->getStatusCode()); $response->headers->add($exception->getHeaders()); } $event->setResponse($response); }
/** * Tests the formatBacktrace() method. * * @param array $backtrace * The test backtrace array. * @param array $expected * The expected return array. * * @dataProvider providerTestFormatBacktrace */ public function testFormatBacktrace($backtrace, $expected) { $this->assertSame($expected, Error::formatBacktrace($backtrace)); }
/** * Makes a subrequest to retrieve the default error page. * * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event * The event to process * @param string $url * The path/url to which to make a subrequest for this error message. * @param int $status_code * The status code for the error being handled. */ protected function makeSubrequest(GetResponseForExceptionEvent $event, $url, $status_code) { $request = $event->getRequest(); if (!($url && $url[0] == '/')) { $url = $request->getBasePath() . '/' . $url; } $current_url = $request->getBasePath() . $request->getPathInfo(); if ($url != $request->getBasePath() . '/' && $url != $current_url) { if ($request->getMethod() === 'POST') { $sub_request = Request::create($url, 'POST', $this->drupalGetDestination() + ['_exception_statuscode' => $status_code] + $request->request->all(), $request->cookies->all(), [], $request->server->all()); } else { $sub_request = Request::create($url, 'GET', $request->query->all() + $this->drupalGetDestination() + ['_exception_statuscode' => $status_code], $request->cookies->all(), [], $request->server->all()); } try { // Persist the 'exception' attribute to the subrequest. $sub_request->attributes->set('exception', $request->attributes->get('exception')); // Carry over the session to the subrequest. if ($session = $request->getSession()) { $sub_request->setSession($session); } $response = $this->httpKernel->handle($sub_request, HttpKernelInterface::SUB_REQUEST); $response->setStatusCode($status_code); $event->setResponse($response); } catch (\Exception $e) { // If an error happened in the subrequest we can't do much else. Instead, // just log it. The DefaultExceptionSubscriber will catch the original // exception and handle it normally. $error = Error::decodeException($e); $this->logger->log($error['severity_level'], '%type: !message in %function (line %line of %file).', $error); } } }