Completed
Pull Request — master (#312)
by Denis
07:05 queued 04:57
created

Run::handleExceptionFromPHP()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.9766

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 14
ccs 3
cts 8
cp 0.375
rs 9.4286
cc 2
eloc 7
nc 2
nop 1
crap 2.9766
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 Exception;
10
use InvalidArgumentException;
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
17
class Run
18
{
19
    const EXCEPTION_HANDLER = "handleExceptionFromPHP";
20
    const ERROR_HANDLER     = "handleError";
21
    const SHUTDOWN_HANDLER  = "handleShutdown";
22
23
    protected $isRegistered;
24
    protected $allowQuit       = true;
25
    protected $sendOutput      = true;
26
27
    /**
28
     * @var integer|false
29
     */
30
    protected $sendHttpCode    = 500;
31
32
    /**
33
     * @var HandlerInterface[]
34
     */
35
    protected $handlerStack = array();
36
37
    protected $silencedPatterns = array();
38
39
    /**
40
     * Pushes a handler to the end of the stack
41
     *
42
     * @throws InvalidArgumentException  If argument is not callable or instance of HandlerInterface
43
     * @param  Callable|HandlerInterface $handler
44
     * @return Run
45
     */
46 10
    public function pushHandler($handler)
47
    {
48 10
        if (is_callable($handler)) {
49 1
            $handler = new CallbackHandler($handler);
50 1
        }
51
52 10
        if (!$handler instanceof HandlerInterface) {
53 1
            throw new InvalidArgumentException(
54 1
                  "Argument to " . __METHOD__ . " must be a callable, or instance of"
55 1
                . "Whoops\\Handler\\HandlerInterface"
56 1
            );
57
        }
58
59 9
        $this->handlerStack[] = $handler;
60 9
        return $this;
61
    }
62
63
    /**
64
     * Removes the last handler in the stack and returns it.
65
     * Returns null if there"s nothing else to pop.
66
     * @return null|HandlerInterface
67
     */
68 1
    public function popHandler()
69
    {
70 1
        return array_pop($this->handlerStack);
71
    }
72
73
    /**
74
     * Returns an array with all handlers, in the
75
     * order they were added to the stack.
76
     * @return array
77
     */
78 2
    public function getHandlers()
79
    {
80 2
        return $this->handlerStack;
81
    }
82
83
    /**
84
     * Clears all handlers in the handlerStack, including
85
     * the default PrettyPage handler.
86
     * @return Run
87
     */
88 1
    public function clearHandlers()
89
    {
90 1
        $this->handlerStack = array();
91 1
        return $this;
92
    }
93
94
    /**
95
     * @param  Exception $exception
96
     * @return Inspector
97
     */
98 3
    protected function getInspector(Exception $exception)
99
    {
100 3
        return new Inspector($exception);
101
    }
102
103
    /**
104
     * Registers this instance as an error handler.
105
     * @return Run
106
     */
107 5
    public function register()
108
    {
109 5
        if (!$this->isRegistered) {
110
            // Workaround PHP bug 42098
111
            // https://bugs.php.net/bug.php?id=42098
112 5
            class_exists("\\Whoops\\Exception\\ErrorException");
113 5
            class_exists("\\Whoops\\Exception\\FrameCollection");
114 5
            class_exists("\\Whoops\\Exception\\Frame");
115 5
            class_exists("\\Whoops\\Exception\\Inspector");
116
117 5
            set_error_handler(array($this, self::ERROR_HANDLER));
118 5
            set_exception_handler(array($this, self::EXCEPTION_HANDLER));
119 5
            register_shutdown_function(array($this, self::SHUTDOWN_HANDLER));
120
121 5
            $this->isRegistered = true;
122 5
        }
123
124 5
        return $this;
125
    }
126
127
    /**
128
     * Unregisters all handlers registered by this Whoops\Run instance
129
     * @return Run
130
     */
131 1
    public function unregister()
132
    {
133 1
        if ($this->isRegistered) {
134 1
            restore_exception_handler();
135 1
            restore_error_handler();
136
137 1
            $this->isRegistered = false;
138 1
        }
139
140 1
        return $this;
141
    }
142
143
    /**
144
     * Should Whoops allow Handlers to force the script to quit?
145
     * @param  bool|int $exit
146
     * @return bool
147
     */
148 6
    public function allowQuit($exit = null)
149
    {
150 6
        if (func_num_args() == 0) {
151 2
            return $this->allowQuit;
152
        }
153
154 6
        return $this->allowQuit = (bool) $exit;
155
    }
156
157
    /**
158
     * Silence particular errors in particular files
159
     * @param  array|string $patterns List or a single regex pattern to match
160
     * @param  int          $levels   Defaults to E_STRICT | E_DEPRECATED
161
     * @return \Whoops\Run
162
     */
163 1
    public function silenceErrorsInPaths($patterns, $levels = 10240)
164
    {
165 1
        $this->silencedPatterns = array_merge(
166 1
            $this->silencedPatterns,
167 1
            array_map(
168 1
                function ($pattern) use ($levels) {
169
                    return array(
170 1
                        "pattern" => $pattern,
171 1
                        "levels" => $levels,
172 1
                    );
173 1
                },
174
                (array) $patterns
175 1
            )
176 1
        );
177 1
        return $this;
178
    }
179
180
    /*
181
     * Should Whoops send HTTP error code to the browser if possible?
182
     * Whoops will by default send HTTP code 500, but you may wish to
183
     * use 502, 503, or another 5xx family code.
184
     *
185
     * @param bool|int $code
186
     * @return int|false
187
     */
188 5
    public function sendHttpCode($code = null)
189
    {
190 5
        if (func_num_args() == 0) {
191 4
            return $this->sendHttpCode;
192
        }
193
194 2
        if (!$code) {
195
            return $this->sendHttpCode = false;
196
        }
197
198 2
        if ($code === true) {
199 1
            $code = 500;
200 1
        }
201
202 2
        if ($code < 400 || 600 <= $code) {
203 1
            throw new InvalidArgumentException(
204 1
                 "Invalid status code '$code', must be 4xx or 5xx"
205 1
            );
206
        }
207
208 1
        return $this->sendHttpCode = $code;
209
    }
210
211
    /**
212
     * Should Whoops push output directly to the client?
213
     * If this is false, output will be returned by handleException
214
     * @param  bool|int $send
215
     * @return bool
216
     */
217 5
    public function writeToOutput($send = null)
218
    {
219 5
        if (func_num_args() == 0) {
220 5
            return $this->sendOutput;
221
        }
222
223 1
        return $this->sendOutput = (bool) $send;
224
    }
225
226
    /**
227
     * Handles an exception, ultimately generating a Whoops error
228
     * page.
229
     *
230
     * @param  Exception $exception
231
     * @return string    Output generated by handlers
232
     */
233 7
    public function handleException(Exception $exception)
234
    {
235
        // Walk the registered handlers in the reverse order
236
        // they were registered, and pass off the exception
237 7
        $inspector = $this->getInspector($exception);
238
239
        // Capture output produced while handling the exception,
240
        // we might want to send it straight away to the client,
241
        // or return it silently.
242 7
        ob_start();
243
244
        // Just in case there are no handlers:
245 7
        $handlerResponse = null;
246
247 7
        foreach (array_reverse($this->handlerStack) as $handler) {
248 7
            $handler->setRun($this);
249 7
            $handler->setInspector($inspector);
250 7
            $handler->setException($exception);
251
252
            // The HandlerInterface does not require an Exception passed to handle()
253
            // and neither of our bundled handlers use it.
254
            // However, 3rd party handlers may have already relied on this parameter,
255
            // and removing it would be possibly breaking for users.
256 7
            $handlerResponse = $handler->handle($exception);
257
258 7
            if (in_array($handlerResponse, array(Handler::LAST_HANDLER, Handler::QUIT))) {
259
                // The Handler has handled the exception in some way, and
260
                // wishes to quit execution (Handler::QUIT), or skip any
261
                // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
262
                // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
263 3
                break;
264
            }
265 7
        }
266
267 7
        $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
268
269 7
        $output = ob_get_clean();
270
271
        // If we're allowed to, send output generated by handlers directly
272
        // to the output, otherwise, and if the script doesn't quit, return
273
        // it so that it may be used by the caller
274 7
        if ($this->writeToOutput()) {
275
            // @todo Might be able to clean this up a bit better
276
            // If we're going to quit execution, cleanup all other output
277
            // buffers before sending our own output:
278 6
            if ($willQuit) {
279
                while (ob_get_level() > 0) {
280
                    ob_end_clean();
281
                }
282
            }
283
284 6
            $this->writeToOutputNow($output);
285 6
        }
286
287 7
        if ($willQuit) {
288
            flush(); // HHVM fix for https://github.com/facebook/hhvm/issues/4055
289
            exit(1);
290
        }
291
292 7
        return $output;
293
    }
294
295
    /**
296
     * This is a private implementation detail, you are not supposed to call this.
297
     */
298 1
    final public function handleExceptionFromPHP($throwable)
299
    {
300 1
        if (self::isPHP7Throwable($throwable)) {
301
            // Compatibility with Whoops children that expect the exception
302
            // to always be a PHP 5 Exception class
303
            $throwable = new Exception(
304
                get_class($throwable) . ': ' . $throwable->getMessage(),
305
                $throwable->getCode(),
306
                $throwable
307
            );
308
        }
309
310 1
        return $this->handleException($throwable);
311
    }
312
313
    /**
314
     * Converts generic PHP errors to \ErrorException
315
     * instances, before passing them off to be handled.
316
     *
317
     * This method MUST be compatible with set_error_handler.
318
     *
319
     * @param int    $level
320
     * @param string $message
321
     * @param string $file
322
     * @param int    $line
323
     *
324
     * @return bool
325
     * @throws ErrorException
326
     */
327 5
    public function handleError($level, $message, $file = null, $line = null)
328
    {
329 5
        if ($level & error_reporting()) {
330 2
            foreach ($this->silencedPatterns as $entry) {
331
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
332
                $levelMatches = $level & $entry["levels"];
333
                if ($pathMatches && $levelMatches) {
334
                    // Ignore the error, abort handling
335
                    return true;
336
                }
337 2
            }
338
339
            // XXX we pass $level for the "code" param only for BC reasons.
340
            // see https://github.com/filp/whoops/issues/267
341 2
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
342 2
            if ($this->canThrowExceptions) {
343 2
                throw $exception;
344
            } else {
345
                $this->handleException($exception);
346
            }
347
            // Do not propagate errors which were already handled by Whoops.
348
            return true;
349
        }
350
351
        // Propagate error to the next handler, allows error_get_last() to
352
        // work on silenced errors.
353 3
        return false;
354
    }
355
356
    /**
357
     * Special case to deal with Fatal errors and the like.
358
     */
359
    public function handleShutdown()
360
    {
361
        // If we reached this step, we are in shutdown handler.
362
        // An exception thrown in a shutdown handler will not be propagated
363
        // to the exception handler. Pass that information along.
364
        $this->canThrowExceptions = false;
365
366
        $error = error_get_last();
367
        if ($error && $this->isLevelFatal($error['type'])) {
368
            // If there was a fatal error,
369
            // it was not handled in handleError yet.
370
            $this->handleError(
371
                $error['type'],
372
                $error['message'],
373
                $error['file'],
374
                $error['line']
375
            );
376
        }
377
    }
378
379
    /**
380
     * In certain scenarios, like in shutdown handler, we can not throw exceptions
381
     * @var bool
382
     */
383
    private $canThrowExceptions = true;
384
385
    /**
386
     * Echo something to the browser
387
     * @param  string $output
388
     * @return $this
389
     */
390 3
    private function writeToOutputNow($output)
391
    {
392 3
        if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) {
393
            $httpCode   = $this->sendHttpCode();
394
395
            if (function_exists('http_response_code')) {
396
                http_response_code($httpCode);
397
            } else {
398
                // http_response_code is added in 5.4.
399
                // For compatibility with 5.3 we use the third argument in header call
400
                // First argument must be a real header.
401
                // If it is empty, PHP will ignore the third argument.
402
                // If it is invalid, such as a single space, Apache will handle it well,
403
                // but the PHP development server will hang.
404
                // Setting a full status line would require us to hardcode
405
                // string values for all different status code, and detect the protocol.
406
                // which is an extra error-prone complexity.
407
                header('X-Ignore-This: 1', true, $httpCode);
408
            }
409
        }
410
411 3
        echo $output;
412
413 3
        return $this;
414
    }
415
416 1
    private static function isPHP7Throwable($something)
417
    {
418 1
        return !$something instanceof \Exception &&
419 1
            interface_exists('Throwable', false) &&
420 1
            !is_subclass_of('Throwable', 'Exception') &&
421 1
            $something instanceof \Throwable;
1 ignored issue
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
422
    }
423
424
    private static function isLevelFatal($level)
425
    {
426
        $errors = E_ERROR;
427
        $errors |= E_PARSE;
428
        $errors |= E_CORE_ERROR;
429
        $errors |= E_CORE_WARNING;
430
        $errors |= E_COMPILE_ERROR;
431
        $errors |= E_COMPILE_WARNING;
432
        return ($level & $errors) > 0;
433
    }
434
}
435