Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
/**
4
 * Slim Framework (https://slimframework.com)
5
 *
6
 * @license https://github.com/slimphp/Slim/blob/4.x/LICENSE.md (MIT License)
7
 */
8
 
9
declare(strict_types=1);
10
 
11
namespace Slim\Handlers;
12
 
13
use Psr\Http\Message\ResponseFactoryInterface;
14
use Psr\Http\Message\ResponseInterface;
15
use Psr\Http\Message\ServerRequestInterface;
16
use Psr\Log\LoggerInterface;
17
use RuntimeException;
18
use Slim\Error\Renderers\HtmlErrorRenderer;
19
use Slim\Error\Renderers\JsonErrorRenderer;
20
use Slim\Error\Renderers\PlainTextErrorRenderer;
21
use Slim\Error\Renderers\XmlErrorRenderer;
22
use Slim\Exception\HttpException;
23
use Slim\Exception\HttpMethodNotAllowedException;
24
use Slim\Interfaces\CallableResolverInterface;
25
use Slim\Interfaces\ErrorHandlerInterface;
26
use Slim\Interfaces\ErrorRendererInterface;
27
use Slim\Logger;
28
use Throwable;
29
 
30
use function array_intersect;
31
use function array_key_exists;
32
use function array_keys;
33
use function call_user_func;
34
use function count;
35
use function current;
36
use function explode;
37
use function implode;
38
use function next;
39
use function preg_match;
40
 
41
/**
42
 * Default Slim application error handler
43
 *
44
 * It outputs the error message and diagnostic information in one of the following formats:
45
 * JSON, XML, Plain Text or HTML based on the Accept header.
46
 */
47
class ErrorHandler implements ErrorHandlerInterface
48
{
49
    protected string $defaultErrorRendererContentType = 'text/html';
50
 
51
    /**
52
     * @var ErrorRendererInterface|string|callable
53
     */
54
    protected $defaultErrorRenderer = HtmlErrorRenderer::class;
55
 
56
    /**
57
     * @var ErrorRendererInterface|string|callable
58
     */
59
    protected $logErrorRenderer = PlainTextErrorRenderer::class;
60
 
61
    /**
62
     * @var array<string|callable>
63
     */
64
    protected array $errorRenderers = [
65
        'application/json' => JsonErrorRenderer::class,
66
        'application/xml' => XmlErrorRenderer::class,
67
        'text/xml' => XmlErrorRenderer::class,
68
        'text/html' => HtmlErrorRenderer::class,
69
        'text/plain' => PlainTextErrorRenderer::class,
70
    ];
71
 
72
    protected bool $displayErrorDetails = false;
73
 
74
    protected bool $logErrors;
75
 
76
    protected bool $logErrorDetails = false;
77
 
78
    protected ?string $contentType = null;
79
 
80
    protected ?string $method = null;
81
 
82
    protected ServerRequestInterface $request;
83
 
84
    protected Throwable $exception;
85
 
86
    protected int $statusCode;
87
 
88
    protected CallableResolverInterface $callableResolver;
89
 
90
    protected ResponseFactoryInterface $responseFactory;
91
 
92
    protected LoggerInterface $logger;
93
 
94
    public function __construct(
95
        CallableResolverInterface $callableResolver,
96
        ResponseFactoryInterface $responseFactory,
97
        ?LoggerInterface $logger = null
98
    ) {
99
        $this->callableResolver = $callableResolver;
100
        $this->responseFactory = $responseFactory;
101
        $this->logger = $logger ?: $this->getDefaultLogger();
102
    }
103
 
104
    /**
105
     * Invoke error handler
106
     *
107
     * @param ServerRequestInterface $request             The most recent Request object
108
     * @param Throwable              $exception           The caught Exception object
109
     * @param bool                   $displayErrorDetails Whether or not to display the error details
110
     * @param bool                   $logErrors           Whether or not to log errors
111
     * @param bool                   $logErrorDetails     Whether or not to log error details
112
     */
113
    public function __invoke(
114
        ServerRequestInterface $request,
115
        Throwable $exception,
116
        bool $displayErrorDetails,
117
        bool $logErrors,
118
        bool $logErrorDetails
119
    ): ResponseInterface {
120
        $this->displayErrorDetails = $displayErrorDetails;
121
        $this->logErrors = $logErrors;
122
        $this->logErrorDetails = $logErrorDetails;
123
        $this->request = $request;
124
        $this->exception = $exception;
125
        $this->method = $request->getMethod();
126
        $this->statusCode = $this->determineStatusCode();
127
        if ($this->contentType === null) {
128
            $this->contentType = $this->determineContentType($request);
129
        }
130
 
131
        if ($logErrors) {
132
            $this->writeToErrorLog();
133
        }
134
 
135
        return $this->respond();
136
    }
137
 
138
    /**
139
     * Force the content type for all error handler responses.
140
     *
141
     * @param string|null $contentType The content type
142
     */
143
    public function forceContentType(?string $contentType): void
144
    {
145
        $this->contentType = $contentType;
146
    }
147
 
148
    protected function determineStatusCode(): int
149
    {
150
        if ($this->method === 'OPTIONS') {
151
            return 200;
152
        }
153
 
154
        if ($this->exception instanceof HttpException) {
155
            return $this->exception->getCode();
156
        }
157
 
158
        return 500;
159
    }
160
 
161
    /**
162
     * Determine which content type we know about is wanted using Accept header
163
     *
164
     * Note: This method is a bare-bones implementation designed specifically for
165
     * Slim's error handling requirements. Consider a fully-feature solution such
166
     * as willdurand/negotiation for any other situation.
167
     */
168
    protected function determineContentType(ServerRequestInterface $request): ?string
169
    {
170
        $acceptHeader = $request->getHeaderLine('Accept');
171
        $selectedContentTypes = array_intersect(
172
            explode(',', $acceptHeader),
173
            array_keys($this->errorRenderers)
174
        );
175
        $count = count($selectedContentTypes);
176
 
177
        if ($count) {
178
            $current = current($selectedContentTypes);
179
 
180
            /**
181
             * Ensure other supported content types take precedence over text/plain
182
             * when multiple content types are provided via Accept header.
183
             */
184
            if ($current === 'text/plain' && $count > 1) {
185
                $next = next($selectedContentTypes);
186
                if (is_string($next)) {
187
                    return $next;
188
                }
189
            }
190
 
191
            if (is_string($current)) {
192
                return $current;
193
            }
194
        }
195
 
196
        if (preg_match('/\+(json|xml)/', $acceptHeader, $matches)) {
197
            $mediaType = 'application/' . $matches[1];
198
            if (array_key_exists($mediaType, $this->errorRenderers)) {
199
                return $mediaType;
200
            }
201
        }
202
 
203
        return null;
204
    }
205
 
206
    /**
207
     * Determine which renderer to use based on content type
208
     *
209
     * @throws RuntimeException
210
     */
211
    protected function determineRenderer(): callable
212
    {
213
        if ($this->contentType !== null && array_key_exists($this->contentType, $this->errorRenderers)) {
214
            $renderer = $this->errorRenderers[$this->contentType];
215
        } else {
216
            $renderer = $this->defaultErrorRenderer;
217
        }
218
 
219
        return $this->callableResolver->resolve($renderer);
220
    }
221
 
222
    /**
223
     * Register an error renderer for a specific content-type
224
     *
225
     * @param string  $contentType  The content-type this renderer should be registered to
226
     * @param ErrorRendererInterface|string|callable $errorRenderer The error renderer
227
     */
228
    public function registerErrorRenderer(string $contentType, $errorRenderer): void
229
    {
230
        $this->errorRenderers[$contentType] = $errorRenderer;
231
    }
232
 
233
    /**
234
     * Set the default error renderer
235
     *
236
     * @param string                                 $contentType   The content type of the default error renderer
237
     * @param ErrorRendererInterface|string|callable $errorRenderer The default error renderer
238
     */
239
    public function setDefaultErrorRenderer(string $contentType, $errorRenderer): void
240
    {
241
        $this->defaultErrorRendererContentType = $contentType;
242
        $this->defaultErrorRenderer = $errorRenderer;
243
    }
244
 
245
    /**
246
     * Set the renderer for the error logger
247
     *
248
     * @param ErrorRendererInterface|string|callable $logErrorRenderer
249
     */
250
    public function setLogErrorRenderer($logErrorRenderer): void
251
    {
252
        $this->logErrorRenderer = $logErrorRenderer;
253
    }
254
 
255
    /**
256
     * Write to the error log if $logErrors has been set to true
257
     */
258
    protected function writeToErrorLog(): void
259
    {
260
        $renderer = $this->callableResolver->resolve($this->logErrorRenderer);
261
        $error = $renderer($this->exception, $this->logErrorDetails);
262
        if (!$this->displayErrorDetails) {
263
            $error .= "\nTips: To display error details in HTTP response ";
264
            $error .= 'set "displayErrorDetails" to true in the ErrorHandler constructor.';
265
        }
266
        $this->logError($error);
267
    }
268
 
269
    /**
270
     * Wraps the error_log function so that this can be easily tested
271
     */
272
    protected function logError(string $error): void
273
    {
274
        $this->logger->error($error);
275
    }
276
 
277
    /**
278
     * Returns a default logger implementation.
279
     */
280
    protected function getDefaultLogger(): LoggerInterface
281
    {
282
        return new Logger();
283
    }
284
 
285
    protected function respond(): ResponseInterface
286
    {
287
        $response = $this->responseFactory->createResponse($this->statusCode);
288
        if ($this->contentType !== null && array_key_exists($this->contentType, $this->errorRenderers)) {
289
            $response = $response->withHeader('Content-type', $this->contentType);
290
        } else {
291
            $response = $response->withHeader('Content-type', $this->defaultErrorRendererContentType);
292
        }
293
 
294
        if ($this->exception instanceof HttpMethodNotAllowedException) {
295
            $allowedMethods = implode(', ', $this->exception->getAllowedMethods());
296
            $response = $response->withHeader('Allow', $allowedMethods);
297
        }
298
 
299
        $renderer = $this->determineRenderer();
300
        $body = call_user_func($renderer, $this->exception, $this->displayErrorDetails);
301
        if ($body !== false) {
302
            /** @var string $body */
303
            $response->getBody()->write($body);
304
        }
305
 
306
        return $response;
307
    }
308
}