Version 1
[yaffs-website] / web / core / lib / Drupal / Core / EventSubscriber / DefaultExceptionSubscriber.php
1 <?php
2
3 namespace Drupal\Core\EventSubscriber;
4
5 use Drupal\Component\Utility\SafeMarkup;
6 use Drupal\Core\Config\ConfigFactoryInterface;
7 use Drupal\Core\StringTranslation\StringTranslationTrait;
8 use Drupal\Core\Utility\Error;
9 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
10 use Symfony\Component\HttpFoundation\JsonResponse;
11 use Symfony\Component\HttpFoundation\Request;
12 use Symfony\Component\HttpFoundation\Response;
13 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
14 use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
15 use Symfony\Component\HttpKernel\KernelEvents;
16
17 /**
18  * Last-chance handler for exceptions.
19  *
20  * This handler will catch any exceptions not caught elsewhere and report
21  * them as an error page.
22  */
23 class DefaultExceptionSubscriber implements EventSubscriberInterface {
24   use StringTranslationTrait;
25
26   /**
27    * @var string
28    *
29    * One of the error level constants defined in bootstrap.inc.
30    */
31   protected $errorLevel;
32
33   /**
34    * The config factory.
35    *
36    * @var \Drupal\Core\Config\ConfigFactoryInterface
37    */
38   protected $configFactory;
39
40   /**
41    * Constructs a new DefaultExceptionSubscriber.
42    *
43    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
44    *   The configuration factory.
45    */
46   public function __construct(ConfigFactoryInterface $config_factory) {
47     $this->configFactory = $config_factory;
48   }
49
50   /**
51    * Gets the configured error level.
52    *
53    * @return string
54    */
55   protected function getErrorLevel() {
56     if (!isset($this->errorLevel)) {
57       $this->errorLevel = $this->configFactory->get('system.logging')->get('error_level');
58     }
59     return $this->errorLevel;
60   }
61
62   /**
63    * Handles any exception as a generic error page for HTML.
64    *
65    * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
66    *   The event to process.
67    */
68   protected function onHtml(GetResponseForExceptionEvent $event) {
69     $exception = $event->getException();
70     $error = Error::decodeException($exception);
71
72     // Display the message if the current error reporting level allows this type
73     // of message to be displayed, and unconditionally in update.php.
74     $message = '';
75     if (error_displayable($error)) {
76       // If error type is 'User notice' then treat it as debug information
77       // instead of an error message.
78       // @see debug()
79       if ($error['%type'] == 'User notice') {
80         $error['%type'] = 'Debug';
81       }
82
83       // Attempt to reduce verbosity by removing DRUPAL_ROOT from the file path
84       // in the message. This does not happen for (false) security.
85       $root_length = strlen(DRUPAL_ROOT);
86       if (substr($error['%file'], 0, $root_length) == DRUPAL_ROOT) {
87         $error['%file'] = substr($error['%file'], $root_length + 1);
88       }
89
90       unset($error['backtrace']);
91
92       if ($this->getErrorLevel() != ERROR_REPORTING_DISPLAY_VERBOSE) {
93         // Without verbose logging, use a simple message.
94
95         // We call SafeMarkup::format directly here, rather than use t() since
96         // we are in the middle of error handling, and we don't want t() to
97         // cause further errors.
98         $message = SafeMarkup::format('%type: @message in %function (line %line of %file).', $error);
99       }
100       else {
101         // With verbose logging, we will also include a backtrace.
102
103         $backtrace_exception = $exception;
104         while ($backtrace_exception->getPrevious()) {
105           $backtrace_exception = $backtrace_exception->getPrevious();
106         }
107         $backtrace = $backtrace_exception->getTrace();
108         // First trace is the error itself, already contained in the message.
109         // While the second trace is the error source and also contained in the
110         // message, the message doesn't contain argument values, so we output it
111         // once more in the backtrace.
112         array_shift($backtrace);
113
114         // Generate a backtrace containing only scalar argument values.
115         $error['@backtrace'] = Error::formatBacktrace($backtrace);
116         $message = SafeMarkup::format('%type: @message in %function (line %line of %file). <pre class="backtrace">@backtrace</pre>', $error);
117       }
118     }
119
120     $content = $this->t('The website encountered an unexpected error. Please try again later.');
121     $content .= $message ? '</br></br>' . $message : '';
122     $response = new Response($content, 500);
123
124     if ($exception instanceof HttpExceptionInterface) {
125       $response->setStatusCode($exception->getStatusCode());
126       $response->headers->add($exception->getHeaders());
127     }
128     else {
129       $response->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR, '500 Service unavailable (with message)');
130     }
131
132     $event->setResponse($response);
133   }
134
135   /**
136    * Handles any exception as a generic error page for JSON.
137    *
138    * @todo This should probably check the error reporting level.
139    *
140    * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
141    *   The event to process.
142    */
143   protected function onJson(GetResponseForExceptionEvent $event) {
144     $exception = $event->getException();
145     $error = Error::decodeException($exception);
146
147     // Display the message if the current error reporting level allows this type
148     // of message to be displayed,
149     $data = NULL;
150     if (error_displayable($error) && $message = $exception->getMessage()) {
151       $data = ['message' => sprintf('A fatal error occurred: %s', $message)];
152     }
153
154     $response = new JsonResponse($data, Response::HTTP_INTERNAL_SERVER_ERROR);
155     if ($exception instanceof HttpExceptionInterface) {
156       $response->setStatusCode($exception->getStatusCode());
157       $response->headers->add($exception->getHeaders());
158     }
159
160     $event->setResponse($response);
161   }
162
163   /**
164    * Handles an HttpExceptionInterface exception for unknown formats.
165    *
166    * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
167    *   The event to process.
168    */
169   protected function onFormatUnknown(GetResponseForExceptionEvent $event) {
170     /** @var \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface|\Exception $exception */
171     $exception = $event->getException();
172
173     $response = new Response($exception->getMessage(), $exception->getStatusCode(), $exception->getHeaders());
174     $event->setResponse($response);
175   }
176
177   /**
178    * Handles errors for this subscriber.
179    *
180    * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event
181    *   The event to process.
182    */
183   public function onException(GetResponseForExceptionEvent $event) {
184     $format = $this->getFormat($event->getRequest());
185     $exception = $event->getException();
186
187     $method = 'on' . $format;
188     if (!method_exists($this, $method)) {
189       if ($exception instanceof HttpExceptionInterface) {
190         $this->onFormatUnknown($event);
191         $response = $event->getResponse();
192         $response->headers->set('Content-Type', 'text/plain');
193       }
194       else {
195         $this->onHtml($event);
196       }
197     }
198     else {
199       $this->$method($event);
200     }
201   }
202
203   /**
204    * Gets the error-relevant format from the request.
205    *
206    * @param \Symfony\Component\HttpFoundation\Request $request
207    *   The request object.
208    *
209    * @return string
210    *   The format as which to treat the exception.
211    */
212   protected function getFormat(Request $request) {
213     $format = $request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT, $request->getRequestFormat());
214
215     // These are all JSON errors for our purposes. Any special handling for
216     // them can/should happen in earlier listeners if desired.
217     if (in_array($format, ['drupal_modal', 'drupal_dialog', 'drupal_ajax'])) {
218       $format = 'json';
219     }
220
221     // Make an educated guess that any Accept header type that includes "json"
222     // can probably handle a generic JSON response for errors. As above, for
223     // any format this doesn't catch or that wants custom handling should
224     // register its own exception listener.
225     foreach ($request->getAcceptableContentTypes() as $mime) {
226       if (strpos($mime, 'html') === FALSE && strpos($mime, 'json') !== FALSE) {
227         $format = 'json';
228       }
229     }
230
231     return $format;
232   }
233
234   /**
235    * Registers the methods in this class that should be listeners.
236    *
237    * @return array
238    *   An array of event listener definitions.
239    */
240   public static function getSubscribedEvents() {
241     $events[KernelEvents::EXCEPTION][] = ['onException', -256];
242     return $events;
243   }
244
245 }