Completed
Pull Request — v1 (#450)
by Denis
02:54
created

Run::isLevelFatal()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 10
ccs 0
cts 8
cp 0
rs 9.4286
cc 1
eloc 8
nc 1
nop 1
crap 2
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 Whoops\Exception\ErrorException;
11
use Whoops\Exception\Inspector;
12
use Whoops\Handler\CallbackHandler;
13
use Whoops\Handler\Handler;
14
use Whoops\Handler\HandlerInterface;
15
use Whoops\Util\Misc;
16
use Whoops\Util\SystemFacade;
17
18
final class Run implements RunInterface
19
{
20
    private $isRegistered;
21
    private $allowQuit       = true;
22
    private $sendOutput      = true;
23
24
    /**
25
     * @var integer|false
26
     */
27
    private $sendHttpCode    = 500;
28
29
    /**
30
     * @var HandlerInterface[]
31
     */
32
    private $handlerStack = [];
33
34
    private $silencedPatterns = [];
35
36
    private $system;
37
38 4
    public function __construct(SystemFacade $system = null)
39
    {
40 4
        $this->system = $system ?: new SystemFacade;
41 4
    }
42
43
    /**
44
     * Pushes a handler to the end of the stack
45
     *
46
     * @throws InvalidArgumentException  If argument is not callable or instance of HandlerInterface
47
     * @param  Callable|HandlerInterface $handler
48
     * @return Run
49
     */
50 8
    public function pushHandler($handler)
51
    {
52 8
        if (is_callable($handler)) {
53 1
            $handler = new CallbackHandler($handler);
54 1
        }
55
56 8
        if (!$handler instanceof HandlerInterface) {
57 1
            throw new InvalidArgumentException(
58 1
                  "Argument to " . __METHOD__ . " must be a callable, or instance of "
59 1
                . "Whoops\\Handler\\HandlerInterface"
60 1
            );
61
        }
62
63 7
        $this->handlerStack[] = $handler;
64 7
        return $this;
65
    }
66
67
    /**
68
     * Removes the last handler in the stack and returns it.
69
     * Returns null if there"s nothing else to pop.
70
     * @return null|HandlerInterface
71
     */
72 1
    public function popHandler()
73
    {
74 1
        return array_pop($this->handlerStack);
75
    }
76
77
    /**
78
     * Returns an array with all handlers, in the
79
     * order they were added to the stack.
80
     * @return array
81
     */
82 2
    public function getHandlers()
83
    {
84 2
        return $this->handlerStack;
85
    }
86
87
    /**
88
     * Clears all handlers in the handlerStack, including
89
     * the default PrettyPage handler.
90
     * @return Run
91
     */
92 1
    public function clearHandlers()
93
    {
94 1
        $this->handlerStack = [];
95 1
        return $this;
96
    }
97
98
    /**
99
     * @param  \Throwable $exception
100
     * @return Inspector
101
     */
102 1
    private function getInspector($exception)
103
    {
104 1
        return new Inspector($exception);
105
    }
106
107
    /**
108
     * Registers this instance as an error handler.
109
     * @return Run
110
     */
111 4
    public function register()
112
    {
113 4
        if (!$this->isRegistered) {
114
            // Workaround PHP bug 42098
115
            // https://bugs.php.net/bug.php?id=42098
116 4
            class_exists("\\Whoops\\Exception\\ErrorException");
117 4
            class_exists("\\Whoops\\Exception\\FrameCollection");
118 4
            class_exists("\\Whoops\\Exception\\Frame");
119 4
            class_exists("\\Whoops\\Exception\\Inspector");
120
121 4
            $this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
122 4
            $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
123 4
            $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
124
125 4
            $this->isRegistered = true;
126 4
        }
127
128 4
        return $this;
129
    }
130
131
    /**
132
     * Unregisters all handlers registered by this Whoops\Run instance
133
     * @return Run
134
     */
135 1
    public function unregister()
136
    {
137 1
        if ($this->isRegistered) {
138 1
            $this->system->restoreExceptionHandler();
139 1
            $this->system->restoreErrorHandler();
140
141 1
            $this->isRegistered = false;
142 1
        }
143
144 1
        return $this;
145
    }
146
147
    /**
148
     * Should Whoops allow Handlers to force the script to quit?
149
     * @param  bool|int $exit
150
     * @return bool
151
     */
152 4
    public function allowQuit($exit = null)
153
    {
154 4
        if (func_num_args() == 0) {
155 1
            return $this->allowQuit;
156
        }
157
158 4
        return $this->allowQuit = (bool) $exit;
159
    }
160
161
    /**
162
     * Silence particular errors in particular files
163
     * @param  array|string $patterns List or a single regex pattern to match
164
     * @param  int          $levels   Defaults to E_STRICT | E_DEPRECATED
165
     * @return \Whoops\Run
166
     */
167 1
    public function silenceErrorsInPaths($patterns, $levels = 10240)
168
    {
169 1
        $this->silencedPatterns = array_merge(
170 1
            $this->silencedPatterns,
171 1
            array_map(
172 1
                function ($pattern) use ($levels) {
173
                    return [
174 1
                        "pattern" => $pattern,
175 1
                        "levels" => $levels,
176 1
                    ];
177 1
                },
178
                (array) $patterns
179 1
            )
180 1
        );
181 1
        return $this;
182
    }
183
184
    /*
185
     * Should Whoops send HTTP error code to the browser if possible?
186
     * Whoops will by default send HTTP code 500, but you may wish to
187
     * use 502, 503, or another 5xx family code.
188
     *
189
     * @param bool|int $code
190
     * @return int|false
191
     */
192 3
    public function sendHttpCode($code = null)
193
    {
194 3
        if (func_num_args() == 0) {
195 2
            return $this->sendHttpCode;
196
        }
197
198 2
        if (!$code) {
199
            return $this->sendHttpCode = false;
200
        }
201
202 2
        if ($code === true) {
203 1
            $code = 500;
204 1
        }
205
206 2
        if ($code < 400 || 600 <= $code) {
207 1
            throw new InvalidArgumentException(
208 1
                 "Invalid status code '$code', must be 4xx or 5xx"
209 1
            );
210
        }
211
212 1
        return $this->sendHttpCode = $code;
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->sendHttpCode = $code; of type boolean|integer adds the type boolean to the return on line 212 which is incompatible with the return type declared by the interface Whoops\RunInterface::sendHttpCode of type integer|false.
Loading history...
213
    }
214
215
    /**
216
     * Should Whoops push output directly to the client?
217
     * If this is false, output will be returned by handleException
218
     * @param  bool|int $send
219
     * @return bool
220
     */
221 3
    public function writeToOutput($send = null)
222
    {
223 3
        if (func_num_args() == 0) {
224 3
            return $this->sendOutput;
225
        }
226
227 1
        return $this->sendOutput = (bool) $send;
228
    }
229
230
    /**
231
     * Handles an exception, ultimately generating a Whoops error
232
     * page.
233
     *
234
     * @param  \Throwable $exception
235
     * @return string     Output generated by handlers
236
     */
237 5
    public function handleException($exception)
238
    {
239
        // Walk the registered handlers in the reverse order
240
        // they were registered, and pass off the exception
241 5
        $inspector = $this->getInspector($exception);
242
243
        // Capture output produced while handling the exception,
244
        // we might want to send it straight away to the client,
245
        // or return it silently.
246 5
        $this->system->startOutputBuffering();
247
248
        // Just in case there are no handlers:
249 5
        $handlerResponse = null;
250
251 5
        foreach (array_reverse($this->handlerStack) as $handler) {
252 5
            $handler->setRun($this);
253 5
            $handler->setInspector($inspector);
254 5
            $handler->setException($exception);
255
256
            // The HandlerInterface does not require an Exception passed to handle()
257
            // and neither of our bundled handlers use it.
258
            // However, 3rd party handlers may have already relied on this parameter,
259
            // and removing it would be possibly breaking for users.
260 5
            $handlerResponse = $handler->handle($exception);
261
262 5
            if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
263
                // The Handler has handled the exception in some way, and
264
                // wishes to quit execution (Handler::QUIT), or skip any
265
                // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
266
                // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
267 2
                break;
268
            }
269 5
        }
270
271 5
        $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
272
273 5
        $output = $this->system->cleanOutputBuffer();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->system->cleanOutputBuffer(); of type string|false adds false to the return on line 298 which is incompatible with the return type declared by the interface Whoops\RunInterface::handleException of type string. It seems like you forgot to handle an error condition.
Loading history...
274
275
        // If we're allowed to, send output generated by handlers directly
276
        // to the output, otherwise, and if the script doesn't quit, return
277
        // it so that it may be used by the caller
278 5
        if ($this->writeToOutput()) {
279
            // @todo Might be able to clean this up a bit better
280
            // If we're going to quit execution, cleanup all other output
281
            // buffers before sending our own output:
282 4
            if ($willQuit) {
283
                while ($this->system->getOutputBufferLevel() > 0) {
284
                    $this->system->endOutputBuffering();
285
                }
286
            }
287
288 4
            $this->writeToOutputNow($output);
0 ignored issues
show
Security Bug introduced by
It seems like $output defined by $this->system->cleanOutputBuffer() on line 273 can also be of type false; however, Whoops\Run::writeToOutputNow() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
289 4
        }
290
291 5
        if ($willQuit) {
292
            // HHVM fix for https://github.com/facebook/hhvm/issues/4055
293
            $this->system->flushOutputBuffer();
294
295
            $this->system->stopExecution(1);
296
        }
297
298 5
        return $output;
299
    }
300
301
    /**
302
     * Converts generic PHP errors to \ErrorException
303
     * instances, before passing them off to be handled.
304
     *
305
     * This method MUST be compatible with set_error_handler.
306
     *
307
     * @param int    $level
308
     * @param string $message
309
     * @param string $file
310
     * @param int    $line
311
     *
312
     * @return bool
313
     * @throws ErrorException
314
     */
315 5
    public function handleError($level, $message, $file = null, $line = null)
316
    {
317 5
        if ($level & $this->system->getErrorReportingLevel()) {
318 2
            foreach ($this->silencedPatterns as $entry) {
319
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
320
                $levelMatches = $level & $entry["levels"];
321
                if ($pathMatches && $levelMatches) {
322
                    // Ignore the error, abort handling
323
                    return true;
324
                }
325 2
            }
326
327
            // XXX we pass $level for the "code" param only for BC reasons.
328
            // see https://github.com/filp/whoops/issues/267
329 2
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
330 2
            if ($this->canThrowExceptions) {
331 2
                throw $exception;
332
            } else {
333
                $this->handleException($exception);
0 ignored issues
show
Documentation introduced by
$exception is of type object<Whoops\Exception\ErrorException>, but the function expects a object<Throwable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
334
            }
335
            // Do not propagate errors which were already handled by Whoops.
336
            return true;
337
        }
338
339
        // Propagate error to the next handler, allows error_get_last() to
340
        // work on silenced errors.
341 3
        return false;
342
    }
343
344
    /**
345
     * Special case to deal with Fatal errors and the like.
346
     */
347
    public function handleShutdown()
348
    {
349
        // If we reached this step, we are in shutdown handler.
350
        // An exception thrown in a shutdown handler will not be propagated
351
        // to the exception handler. Pass that information along.
352
        $this->canThrowExceptions = false;
353
354
        $error = $this->system->getLastError();
355
        if ($error && Misc::isLevelFatal($error['type'])) {
356
            // If there was a fatal error,
357
            // it was not handled in handleError yet.
358
            $this->handleError(
359
                $error['type'],
360
                $error['message'],
361
                $error['file'],
362
                $error['line']
363
            );
364
        }
365
    }
366
367
    /**
368
     * In certain scenarios, like in shutdown handler, we can not throw exceptions
369
     * @var bool
370
     */
371
    private $canThrowExceptions = true;
372
373
    /**
374
     * Echo something to the browser
375
     * @param  string $output
376
     * @return $this
377
     */
378 1
    private function writeToOutputNow($output)
379
    {
380 1
        if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) {
381
            $this->system->setHttpResponseCode(
382
                $this->sendHttpCode()
0 ignored issues
show
Bug introduced by
It seems like $this->sendHttpCode() targeting Whoops\Run::sendHttpCode() can also be of type boolean; however, Whoops\Util\SystemFacade::setHttpResponseCode() does only seem to accept integer, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
383
            );
384
        }
385
386 1
        echo $output;
387
388 1
        return $this;
389
    }
390
}
391