1: <?php
2: /**
3: * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4: * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5: *
6: * Licensed under The MIT License
7: * For full copyright and license information, please see the LICENSE.txt
8: * Redistributions of files must retain the above copyright notice.
9: *
10: * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11: * @link https://cakephp.org CakePHP(tm) Project
12: * @since 3.3.0
13: * @license https://opensource.org/licenses/mit-license.php MIT License
14: */
15: namespace Cake\Error\Middleware;
16:
17: use Cake\Core\App;
18: use Cake\Core\Configure;
19: use Cake\Core\Exception\Exception as CakeException;
20: use Cake\Core\InstanceConfigTrait;
21: use Cake\Error\ExceptionRenderer;
22: use Cake\Error\PHP7ErrorException;
23: use Cake\Log\Log;
24: use Error;
25: use Exception;
26: use Throwable;
27:
28: /**
29: * Error handling middleware.
30: *
31: * Traps exceptions and converts them into HTML or content-type appropriate
32: * error pages using the CakePHP ExceptionRenderer.
33: */
34: class ErrorHandlerMiddleware
35: {
36: use InstanceConfigTrait;
37:
38: /**
39: * Default configuration values.
40: *
41: * - `log` Enable logging of exceptions.
42: * - `skipLog` List of exceptions to skip logging. Exceptions that
43: * extend one of the listed exceptions will also not be logged. Example:
44: *
45: * ```
46: * 'skipLog' => ['Cake\Error\NotFoundException', 'Cake\Error\UnauthorizedException']
47: * ```
48: *
49: * - `trace` Should error logs include stack traces?
50: *
51: * @var array
52: */
53: protected $_defaultConfig = [
54: 'skipLog' => [],
55: 'log' => true,
56: 'trace' => false,
57: ];
58:
59: /**
60: * Exception render.
61: *
62: * @var \Cake\Error\ExceptionRendererInterface|callable|string|null
63: */
64: protected $exceptionRenderer;
65:
66: /**
67: * Constructor
68: *
69: * @param string|callable|null $exceptionRenderer The renderer or class name
70: * to use or a callable factory. If null, Configure::read('Error.exceptionRenderer')
71: * will be used.
72: * @param array $config Configuration options to use. If empty, `Configure::read('Error')`
73: * will be used.
74: */
75: public function __construct($exceptionRenderer = null, array $config = [])
76: {
77: if ($exceptionRenderer) {
78: $this->exceptionRenderer = $exceptionRenderer;
79: }
80:
81: $config = $config ?: Configure::read('Error');
82: $this->setConfig($config);
83: }
84:
85: /**
86: * Wrap the remaining middleware with error handling.
87: *
88: * @param \Psr\Http\Message\ServerRequestInterface $request The request.
89: * @param \Psr\Http\Message\ResponseInterface $response The response.
90: * @param callable $next Callback to invoke the next middleware.
91: * @return \Psr\Http\Message\ResponseInterface A response
92: */
93: public function __invoke($request, $response, $next)
94: {
95: try {
96: return $next($request, $response);
97: } catch (Throwable $exception) {
98: return $this->handleException($exception, $request, $response);
99: } catch (Exception $exception) {
100: return $this->handleException($exception, $request, $response);
101: }
102: }
103:
104: /**
105: * Handle an exception and generate an error response
106: *
107: * @param \Exception $exception The exception to handle.
108: * @param \Psr\Http\Message\ServerRequestInterface $request The request.
109: * @param \Psr\Http\Message\ResponseInterface $response The response.
110: * @return \Psr\Http\Message\ResponseInterface A response
111: */
112: public function handleException($exception, $request, $response)
113: {
114: $renderer = $this->getRenderer($exception, $request);
115: try {
116: $res = $renderer->render();
117: $this->logException($request, $exception);
118:
119: return $res;
120: } catch (Throwable $exception) {
121: $this->logException($request, $exception);
122: $response = $this->handleInternalError($response);
123: } catch (Exception $exception) {
124: $this->logException($request, $exception);
125: $response = $this->handleInternalError($response);
126: }
127:
128: return $response;
129: }
130:
131: /**
132: * @param \Psr\Http\Message\ResponseInterface $response The response
133: *
134: * @return \Psr\Http\Message\ResponseInterface A response
135: */
136: protected function handleInternalError($response)
137: {
138: $body = $response->getBody();
139: $body->write('An Internal Server Error Occurred');
140:
141: return $response->withStatus(500)
142: ->withBody($body);
143: }
144:
145: /**
146: * Get a renderer instance
147: *
148: * @param \Exception $exception The exception being rendered.
149: * @param \Psr\Http\Message\ServerRequestInterface $request The request.
150: * @return \Cake\Error\ExceptionRendererInterface The exception renderer.
151: * @throws \Exception When the renderer class cannot be found.
152: */
153: protected function getRenderer($exception, $request)
154: {
155: if (!$this->exceptionRenderer) {
156: $this->exceptionRenderer = $this->getConfig('exceptionRenderer') ?: ExceptionRenderer::class;
157: }
158:
159: // For PHP5 backwards compatibility
160: if ($exception instanceof Error) {
161: $exception = new PHP7ErrorException($exception);
162: }
163:
164: if (is_string($this->exceptionRenderer)) {
165: $class = App::className($this->exceptionRenderer, 'Error');
166: if (!$class) {
167: throw new Exception(sprintf(
168: "The '%s' renderer class could not be found.",
169: $this->exceptionRenderer
170: ));
171: }
172:
173: return new $class($exception, $request);
174: }
175: $factory = $this->exceptionRenderer;
176:
177: return $factory($exception, $request);
178: }
179:
180: /**
181: * Log an error for the exception if applicable.
182: *
183: * @param \Psr\Http\Message\ServerRequestInterface $request The current request.
184: * @param \Exception $exception The exception to log a message for.
185: * @return void
186: */
187: protected function logException($request, $exception)
188: {
189: if (!$this->getConfig('log')) {
190: return;
191: }
192:
193: foreach ((array)$this->getConfig('skipLog') as $class) {
194: if ($exception instanceof $class) {
195: return;
196: }
197: }
198:
199: Log::error($this->getMessage($request, $exception));
200: }
201:
202: /**
203: * Generate the error log message.
204: *
205: * @param \Psr\Http\Message\ServerRequestInterface $request The current request.
206: * @param \Exception $exception The exception to log a message for.
207: * @return string Error message
208: */
209: protected function getMessage($request, $exception)
210: {
211: $message = $this->getMessageForException($exception);
212:
213: $message .= "\nRequest URL: " . $request->getRequestTarget();
214: $referer = $request->getHeaderLine('Referer');
215: if ($referer) {
216: $message .= "\nReferer URL: " . $referer;
217: }
218: $message .= "\n\n";
219:
220: return $message;
221: }
222:
223: /**
224: * Generate the message for the exception
225: *
226: * @param \Exception $exception The exception to log a message for.
227: * @param bool $isPrevious False for original exception, true for previous
228: * @return string Error message
229: */
230: protected function getMessageForException($exception, $isPrevious = false)
231: {
232: $message = sprintf(
233: '%s[%s] %s',
234: $isPrevious ? "\nCaused by: " : '',
235: get_class($exception),
236: $exception->getMessage()
237: );
238: $debug = Configure::read('debug');
239:
240: if ($debug && $exception instanceof CakeException) {
241: $attributes = $exception->getAttributes();
242: if ($attributes) {
243: $message .= "\nException Attributes: " . var_export($exception->getAttributes(), true);
244: }
245: }
246:
247: if ($this->getConfig('trace')) {
248: $message .= "\n" . $exception->getTraceAsString();
249: }
250:
251: $previous = $exception->getPrevious();
252: if ($previous) {
253: $message .= $this->getMessageForException($previous, true);
254: }
255:
256: return $message;
257: }
258: }
259: