Subversion-Projekte lars-tiefland.laravel_shop

Revision

Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
/**
3
 * Whoops - php errors for cool kids
4
 * @author Filipe Dobreira <http://github.com/filp>
5
 */
6
 
7
namespace Whoops;
8
 
9
use InvalidArgumentException;
10
use Throwable;
11
use Whoops\Exception\ErrorException;
12
use Whoops\Exception\Inspector;
13
use Whoops\Handler\CallbackHandler;
14
use Whoops\Handler\Handler;
15
use Whoops\Handler\HandlerInterface;
16
use Whoops\Util\Misc;
17
use Whoops\Util\SystemFacade;
18
 
19
final class Run implements RunInterface
20
{
21
    /**
22
     * @var bool
23
     */
24
    private $isRegistered;
25
 
26
    /**
27
     * @var bool
28
     */
29
    private $allowQuit       = true;
30
 
31
    /**
32
     * @var bool
33
     */
34
    private $sendOutput      = true;
35
 
36
    /**
37
     * @var integer|false
38
     */
39
    private $sendHttpCode    = 500;
40
 
41
    /**
42
     * @var integer|false
43
     */
44
    private $sendExitCode    = 1;
45
 
46
    /**
47
     * @var HandlerInterface[]
48
     */
49
    private $handlerStack = [];
50
 
51
    /**
52
     * @var array
53
     * @psalm-var list<array{patterns: string, levels: int}>
54
     */
55
    private $silencedPatterns = [];
56
 
57
    /**
58
     * @var SystemFacade
59
     */
60
    private $system;
61
 
62
    /**
63
     * In certain scenarios, like in shutdown handler, we can not throw exceptions.
64
     *
65
     * @var bool
66
     */
67
    private $canThrowExceptions = true;
68
 
69
    public function __construct(SystemFacade $system = null)
70
    {
71
        $this->system = $system ?: new SystemFacade;
72
    }
73
 
74
    /**
75
     * Explicitly request your handler runs as the last of all currently registered handlers.
76
     *
77
     * @param callable|HandlerInterface $handler
78
     *
79
     * @return Run
80
     */
81
    public function appendHandler($handler)
82
    {
83
        array_unshift($this->handlerStack, $this->resolveHandler($handler));
84
        return $this;
85
    }
86
 
87
    /**
88
     * Explicitly request your handler runs as the first of all currently registered handlers.
89
     *
90
     * @param callable|HandlerInterface $handler
91
     *
92
     * @return Run
93
     */
94
    public function prependHandler($handler)
95
    {
96
        return $this->pushHandler($handler);
97
    }
98
 
99
    /**
100
     * Register your handler as the last of all currently registered handlers (to be executed first).
101
     * Prefer using appendHandler and prependHandler for clarity.
102
     *
103
     * @param callable|HandlerInterface $handler
104
     *
105
     * @return Run
106
     *
107
     * @throws InvalidArgumentException If argument is not callable or instance of HandlerInterface.
108
     */
109
    public function pushHandler($handler)
110
    {
111
        $this->handlerStack[] = $this->resolveHandler($handler);
112
        return $this;
113
    }
114
 
115
    /**
116
     * Removes and returns the last handler pushed to the handler stack.
117
     *
118
     * @see Run::removeFirstHandler(), Run::removeLastHandler()
119
     *
120
     * @return HandlerInterface|null
121
     */
122
    public function popHandler()
123
    {
124
        return array_pop($this->handlerStack);
125
    }
126
 
127
    /**
128
     * Removes the first handler.
129
     *
130
     * @return void
131
     */
132
    public function removeFirstHandler()
133
    {
134
        array_pop($this->handlerStack);
135
    }
136
 
137
    /**
138
     * Removes the last handler.
139
     *
140
     * @return void
141
     */
142
    public function removeLastHandler()
143
    {
144
        array_shift($this->handlerStack);
145
    }
146
 
147
    /**
148
     * Returns an array with all handlers, in the order they were added to the stack.
149
     *
150
     * @return array
151
     */
152
    public function getHandlers()
153
    {
154
        return $this->handlerStack;
155
    }
156
 
157
    /**
158
     * Clears all handlers in the handlerStack, including the default PrettyPage handler.
159
     *
160
     * @return Run
161
     */
162
    public function clearHandlers()
163
    {
164
        $this->handlerStack = [];
165
        return $this;
166
    }
167
 
168
    /**
169
     * Registers this instance as an error handler.
170
     *
171
     * @return Run
172
     */
173
    public function register()
174
    {
175
        if (!$this->isRegistered) {
176
            // Workaround PHP bug 42098
177
            // https://bugs.php.net/bug.php?id=42098
178
            class_exists("\\Whoops\\Exception\\ErrorException");
179
            class_exists("\\Whoops\\Exception\\FrameCollection");
180
            class_exists("\\Whoops\\Exception\\Frame");
181
            class_exists("\\Whoops\\Exception\\Inspector");
182
 
183
            $this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
184
            $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
185
            $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
186
 
187
            $this->isRegistered = true;
188
        }
189
 
190
        return $this;
191
    }
192
 
193
    /**
194
     * Unregisters all handlers registered by this Whoops\Run instance.
195
     *
196
     * @return Run
197
     */
198
    public function unregister()
199
    {
200
        if ($this->isRegistered) {
201
            $this->system->restoreExceptionHandler();
202
            $this->system->restoreErrorHandler();
203
 
204
            $this->isRegistered = false;
205
        }
206
 
207
        return $this;
208
    }
209
 
210
    /**
211
     * Should Whoops allow Handlers to force the script to quit?
212
     *
213
     * @param bool|int $exit
214
     *
215
     * @return bool
216
     */
217
    public function allowQuit($exit = null)
218
    {
219
        if (func_num_args() == 0) {
220
            return $this->allowQuit;
221
        }
222
 
223
        return $this->allowQuit = (bool) $exit;
224
    }
225
 
226
    /**
227
     * Silence particular errors in particular files.
228
     *
229
     * @param array|string $patterns List or a single regex pattern to match.
230
     * @param int          $levels   Defaults to E_STRICT | E_DEPRECATED.
231
     *
232
     * @return Run
233
     */
234
    public function silenceErrorsInPaths($patterns, $levels = 10240)
235
    {
236
        $this->silencedPatterns = array_merge(
237
            $this->silencedPatterns,
238
            array_map(
239
                function ($pattern) use ($levels) {
240
                    return [
241
                        "pattern" => $pattern,
242
                        "levels" => $levels,
243
                    ];
244
                },
245
                (array) $patterns
246
            )
247
        );
248
 
249
        return $this;
250
    }
251
 
252
    /**
253
     * Returns an array with silent errors in path configuration.
254
     *
255
     * @return array
256
     */
257
    public function getSilenceErrorsInPaths()
258
    {
259
        return $this->silencedPatterns;
260
    }
261
 
262
    /**
263
     * Should Whoops send HTTP error code to the browser if possible?
264
     * Whoops will by default send HTTP code 500, but you may wish to
265
     * use 502, 503, or another 5xx family code.
266
     *
267
     * @param bool|int $code
268
     *
269
     * @return int|false
270
     *
271
     * @throws InvalidArgumentException
272
     */
273
    public function sendHttpCode($code = null)
274
    {
275
        if (func_num_args() == 0) {
276
            return $this->sendHttpCode;
277
        }
278
 
279
        if (!$code) {
280
            return $this->sendHttpCode = false;
281
        }
282
 
283
        if ($code === true) {
284
            $code = 500;
285
        }
286
 
287
        if ($code < 400 || 600 <= $code) {
288
            throw new InvalidArgumentException(
289
                "Invalid status code '$code', must be 4xx or 5xx"
290
            );
291
        }
292
 
293
        return $this->sendHttpCode = $code;
294
    }
295
 
296
    /**
297
     * Should Whoops exit with a specific code on the CLI if possible?
298
     * Whoops will exit with 1 by default, but you can specify something else.
299
     *
300
     * @param int $code
301
     *
302
     * @return int
303
     *
304
     * @throws InvalidArgumentException
305
     */
306
    public function sendExitCode($code = null)
307
    {
308
        if (func_num_args() == 0) {
309
            return $this->sendExitCode;
310
        }
311
 
312
        if ($code < 0 || 255 <= $code) {
313
            throw new InvalidArgumentException(
314
                "Invalid status code '$code', must be between 0 and 254"
315
            );
316
        }
317
 
318
        return $this->sendExitCode = (int) $code;
319
    }
320
 
321
    /**
322
     * Should Whoops push output directly to the client?
323
     * If this is false, output will be returned by handleException.
324
     *
325
     * @param bool|int $send
326
     *
327
     * @return bool
328
     */
329
    public function writeToOutput($send = null)
330
    {
331
        if (func_num_args() == 0) {
332
            return $this->sendOutput;
333
        }
334
 
335
        return $this->sendOutput = (bool) $send;
336
    }
337
 
338
    /**
339
     * Handles an exception, ultimately generating a Whoops error page.
340
     *
341
     * @param Throwable $exception
342
     *
343
     * @return string Output generated by handlers.
344
     */
345
    public function handleException($exception)
346
    {
347
        // Walk the registered handlers in the reverse order
348
        // they were registered, and pass off the exception
349
        $inspector = $this->getInspector($exception);
350
 
351
        // Capture output produced while handling the exception,
352
        // we might want to send it straight away to the client,
353
        // or return it silently.
354
        $this->system->startOutputBuffering();
355
 
356
        // Just in case there are no handlers:
357
        $handlerResponse = null;
358
        $handlerContentType = null;
359
 
360
        try {
361
            foreach (array_reverse($this->handlerStack) as $handler) {
362
                $handler->setRun($this);
363
                $handler->setInspector($inspector);
364
                $handler->setException($exception);
365
 
366
                // The HandlerInterface does not require an Exception passed to handle()
367
                // and neither of our bundled handlers use it.
368
                // However, 3rd party handlers may have already relied on this parameter,
369
                // and removing it would be possibly breaking for users.
370
                $handlerResponse = $handler->handle($exception);
371
 
372
                // Collect the content type for possible sending in the headers.
373
                $handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null;
374
 
375
                if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
376
                    // The Handler has handled the exception in some way, and
377
                    // wishes to quit execution (Handler::QUIT), or skip any
378
                    // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
379
                    // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
380
                    break;
381
                }
382
            }
383
 
384
            $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
385
        } finally {
386
            $output = $this->system->cleanOutputBuffer();
387
        }
388
 
389
        // If we're allowed to, send output generated by handlers directly
390
        // to the output, otherwise, and if the script doesn't quit, return
391
        // it so that it may be used by the caller
392
        if ($this->writeToOutput()) {
393
            // @todo Might be able to clean this up a bit better
394
            if ($willQuit) {
395
                // Cleanup all other output buffers before sending our output:
396
                while ($this->system->getOutputBufferLevel() > 0) {
397
                    $this->system->endOutputBuffering();
398
                }
399
 
400
                // Send any headers if needed:
401
                if (Misc::canSendHeaders() && $handlerContentType) {
402
                    header("Content-Type: {$handlerContentType}");
403
                }
404
            }
405
 
406
            $this->writeToOutputNow($output);
407
        }
408
 
409
        if ($willQuit) {
410
            // HHVM fix for https://github.com/facebook/hhvm/issues/4055
411
            $this->system->flushOutputBuffer();
412
 
413
            $this->system->stopExecution(
414
                $this->sendExitCode()
415
            );
416
        }
417
 
418
        return $output;
419
    }
420
 
421
    /**
422
     * Converts generic PHP errors to \ErrorException instances, before passing them off to be handled.
423
     *
424
     * This method MUST be compatible with set_error_handler.
425
     *
426
     * @param int         $level
427
     * @param string      $message
428
     * @param string|null $file
429
     * @param int|null    $line
430
     *
431
     * @return bool
432
     *
433
     * @throws ErrorException
434
     */
435
    public function handleError($level, $message, $file = null, $line = null)
436
    {
437
        if ($level & $this->system->getErrorReportingLevel()) {
438
            foreach ($this->silencedPatterns as $entry) {
439
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
440
                $levelMatches = $level & $entry["levels"];
441
                if ($pathMatches && $levelMatches) {
442
                    // Ignore the error, abort handling
443
                    // See https://github.com/filp/whoops/issues/418
444
                    return true;
445
                }
446
            }
447
 
448
            // XXX we pass $level for the "code" param only for BC reasons.
449
            // see https://github.com/filp/whoops/issues/267
450
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
451
            if ($this->canThrowExceptions) {
452
                throw $exception;
453
            } else {
454
                $this->handleException($exception);
455
            }
456
            // Do not propagate errors which were already handled by Whoops.
457
            return true;
458
        }
459
 
460
        // Propagate error to the next handler, allows error_get_last() to
461
        // work on silenced errors.
462
        return false;
463
    }
464
 
465
    /**
466
     * Special case to deal with Fatal errors and the like.
467
     *
468
     * @return void
469
     */
470
    public function handleShutdown()
471
    {
472
        // If we reached this step, we are in shutdown handler.
473
        // An exception thrown in a shutdown handler will not be propagated
474
        // to the exception handler. Pass that information along.
475
        $this->canThrowExceptions = false;
476
 
477
        $error = $this->system->getLastError();
478
        if ($error && Misc::isLevelFatal($error['type'])) {
479
            // If there was a fatal error,
480
            // it was not handled in handleError yet.
481
            $this->allowQuit = false;
482
            $this->handleError(
483
                $error['type'],
484
                $error['message'],
485
                $error['file'],
486
                $error['line']
487
            );
488
        }
489
    }
490
 
491
    /**
492
     * @param Throwable $exception
493
     *
494
     * @return Inspector
495
     */
496
    private function getInspector($exception)
497
    {
498
        return new Inspector($exception);
499
    }
500
 
501
    /**
502
     * Resolves the giving handler.
503
     *
504
     * @param callable|HandlerInterface $handler
505
     *
506
     * @return HandlerInterface
507
     *
508
     * @throws InvalidArgumentException
509
     */
510
    private function resolveHandler($handler)
511
    {
512
        if (is_callable($handler)) {
513
            $handler = new CallbackHandler($handler);
514
        }
515
 
516
        if (!$handler instanceof HandlerInterface) {
517
            throw new InvalidArgumentException(
518
                "Handler must be a callable, or instance of "
519
                . "Whoops\\Handler\\HandlerInterface"
520
            );
521
        }
522
 
523
        return $handler;
524
    }
525
 
526
    /**
527
     * Echo something to the browser.
528
     *
529
     * @param string $output
530
     *
531
     * @return Run
532
     */
533
    private function writeToOutputNow($output)
534
    {
535
        if ($this->sendHttpCode() && Misc::canSendHeaders()) {
536
            $this->system->setHttpResponseCode(
537
                $this->sendHttpCode()
538
            );
539
        }
540
 
541
        echo $output;
542
 
543
        return $this;
544
    }
545
}