Completed
Push — master ( 583072...ecbc8f )
by Denis
01:20 queued 11s
created

Run::shiftHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 2
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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 6
    public function __construct(SystemFacade $system = null)
39
    {
40 6
        $this->system = $system ?: new SystemFacade;
41 6
    }
42
43
    /**
44
     * Explicitly request your handler runs as the last of all currently registered handlers
45
     */
46
    public function appendHandler($handler)
47
    {
48
        array_unshift($this->handlerStack, $this->resolveHandler($handler));
49
        return $this;
50
    }
51 3
52
    /**
53 3
     * Explicitly request your handler runs as the first of all currently registered handlers
54
     */
55
    public function prependHandler($handler)
56
    {
57
        return $this->pushHandler($handler);
58
    }
59
60
    /**
61
     * Register your handler as the last of all currently registered handlers.
62
     * Prefer using appendHandler and prependHandler for clarity.
63
     *
64
     * @throws InvalidArgumentException  If argument is not callable or instance of HandlerInterface
65
     * @param  Callable|HandlerInterface $handler
66
     * @return Run
67
     */
68
    public function pushHandler($handler)
69
    {
70
        $this->handlerStack[] = $this->resolveHandler($handler);
71
        return $this;
72
    }
73
74
    /**
75
     * Removes the last handler in the stack and returns it.
76 6
     * Returns null if there"s nothing else to pop.
77
     * @return null|HandlerInterface
78 6
     */
79 6
    public function popHandler()
80
    {
81
        return array_pop($this->handlerStack);
82
    }
83
84
    /**
85
     * Returns an array with all handlers, in the
86
     * order they were added to the stack.
87
     * @return array
88
     */
89 6
    public function getHandlers()
90
    {
91 6
        return $this->handlerStack;
92
    }
93
94
    /**
95 6
     * Clears all handlers in the handlerStack, including
96
     * the default PrettyPage handler.
97
     * @return Run
98
     */
99
    public function clearHandlers()
100
    {
101
        $this->handlerStack = [];
102 6
        return $this;
103
    }
104
105
    /**
106
     * @param  \Throwable $exception
107
     * @return Inspector
108
     */
109
    private function getInspector($exception)
110 1
    {
111
        return new Inspector($exception);
112 1
    }
113
114
    /**
115
     * Registers this instance as an error handler.
116
     * @return Run
117
     */
118
    public function register()
119
    {
120
        if (!$this->isRegistered) {
121
            // Workaround PHP bug 42098
122
            // https://bugs.php.net/bug.php?id=42098
123
            class_exists("\\Whoops\\Exception\\ErrorException");
124
            class_exists("\\Whoops\\Exception\\FrameCollection");
125
            class_exists("\\Whoops\\Exception\\Frame");
126
            class_exists("\\Whoops\\Exception\\Inspector");
127
128
            $this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
129
            $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
130 2
            $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
131
132 2
            $this->isRegistered = true;
133
        }
134
135
        return $this;
136
    }
137
138
    /**
139
     * Unregisters all handlers registered by this Whoops\Run instance
140 1
     * @return Run
141
     */
142 1
    public function unregister()
143 1
    {
144
        if ($this->isRegistered) {
145
            $this->system->restoreExceptionHandler();
146
            $this->system->restoreErrorHandler();
147
148
            $this->isRegistered = false;
149
        }
150 3
151
        return $this;
152 3
    }
153
154
    /**
155
     * Should Whoops allow Handlers to force the script to quit?
156
     * @param  bool|int $exit
157
     * @return bool
158
     */
159 6
    public function allowQuit($exit = null)
160
    {
161 6
        if (func_num_args() == 0) {
162
            return $this->allowQuit;
163
        }
164 6
165 6
        return $this->allowQuit = (bool) $exit;
166 6
    }
167 6
168
    /**
169 6
     * Silence particular errors in particular files
170 6
     * @param  array|string $patterns List or a single regex pattern to match
171 6
     * @param  int          $levels   Defaults to E_STRICT | E_DEPRECATED
172
     * @return \Whoops\Run
173 6
     */
174 6
    public function silenceErrorsInPaths($patterns, $levels = 10240)
175
    {
176 6
        $this->silencedPatterns = array_merge(
177
            $this->silencedPatterns,
178
            array_map(
179
                function ($pattern) use ($levels) {
180
                    return [
181
                        "pattern" => $pattern,
182
                        "levels" => $levels,
183 1
                    ];
184
                },
185 1
                (array) $patterns
186 1
            )
187 1
        );
188
        return $this;
189 1
    }
190 1
191
192 1
    /**
193
     * Returns an array with silent errors in path configuration
194
     *
195
     * @return array
196
     */
197
    public function getSilenceErrorsInPaths()
198
    {
199
        return $this->silencedPatterns;
200 6
    }
201
202 6
    /*
203 3
     * Should Whoops send HTTP error code to the browser if possible?
204
     * Whoops will by default send HTTP code 500, but you may wish to
205
     * use 502, 503, or another 5xx family code.
206 6
     *
207
     * @param bool|int $code
208
     * @return int|false
209
     */
210
    public function sendHttpCode($code = null)
211
    {
212
        if (func_num_args() == 0) {
213
            return $this->sendHttpCode;
214
        }
215 1
216
        if (!$code) {
217 1
            return $this->sendHttpCode = false;
218 1
        }
219 1
220 1
        if ($code === true) {
221
            $code = 500;
222 1
        }
223 1
224 1
        if ($code < 400 || 600 <= $code) {
225 1
            throw new InvalidArgumentException(
226
                 "Invalid status code '$code', must be 4xx or 5xx"
227 1
            );
228 1
        }
229 1
230
        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 230 which is incompatible with the return type declared by the interface Whoops\RunInterface::sendHttpCode of type integer|false.
Loading history...
231
    }
232
233
    /**
234
     * Should Whoops push output directly to the client?
235
     * If this is false, output will be returned by handleException
236
     * @param  bool|int $send
237
     * @return bool
238
     */
239
    public function writeToOutput($send = null)
240
    {
241
        if (func_num_args() == 0) {
242
            return $this->sendOutput;
243
        }
244
245
        return $this->sendOutput = (bool) $send;
246
    }
247
248
    /**
249
     * Handles an exception, ultimately generating a Whoops error
250
     * page.
251 6
     *
252
     * @param  \Throwable $exception
253 6
     * @return string     Output generated by handlers
254 4
     */
255
    public function handleException($exception)
256
    {
257 3
        // Walk the registered handlers in the reverse order
258 1
        // they were registered, and pass off the exception
259
        $inspector = $this->getInspector($exception);
260
261 2
        // Capture output produced while handling the exception,
262 1
        // we might want to send it straight away to the client,
263 1
        // or return it silently.
264
        $this->system->startOutputBuffering();
265 2
266 1
        // Just in case there are no handlers:
267 1
        $handlerResponse = null;
268 1
        $handlerContentType = null;
269
270
        try {
271 1
            foreach (array_reverse($this->handlerStack) as $handler) {
272
                $handler->setRun($this);
273
                $handler->setInspector($inspector);
274
                $handler->setException($exception);
275
276
                // The HandlerInterface does not require an Exception passed to handle()
277
                // and neither of our bundled handlers use it.
278
                // However, 3rd party handlers may have already relied on this parameter,
279
                // and removing it would be possibly breaking for users.
280 5
                $handlerResponse = $handler->handle($exception);
281
282 5
                // Collect the content type for possible sending in the headers.
283 5
                $handlerContentType = method_exists($handler, 'contentType') ? $handler->contentType() : null;
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Whoops\Handler\HandlerInterface as the method contentType() does only exist in the following implementations of said interface: Whoops\Handler\JsonResponseHandler, Whoops\Handler\PlainTextHandler, Whoops\Handler\PrettyPageHandler, Whoops\Handler\XmlResponseHandler.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
284
285
                if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
286 1
                    // The Handler has handled the exception in some way, and
287
                    // wishes to quit execution (Handler::QUIT), or skip any
288
                    // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
289
                    // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
290
                    break;
291
                }
292
            }
293
294
            $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
295
        } finally {
296 7
            $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 326 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...
297
        }
298
299
        // If we're allowed to, send output generated by handlers directly
300 7
        // to the output, otherwise, and if the script doesn't quit, return
301
        // it so that it may be used by the caller
302
        if ($this->writeToOutput()) {
303
            // @todo Might be able to clean this up a bit better
304
            if ($willQuit) {
0 ignored issues
show
Bug introduced by
The variable $willQuit does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
305 7
                // Cleanup all other output buffers before sending our output:
306
                while ($this->system->getOutputBufferLevel() > 0) {
307
                    $this->system->endOutputBuffering();
308 7
                }
309 7
310
                // Send any headers if needed:
311
                if (Misc::canSendHeaders() && $handlerContentType) {
312 7
                    header("Content-Type: {$handlerContentType}");
313 7
                }
314 7
            }
315 7
316
            $this->writeToOutputNow($output);
0 ignored issues
show
Security Bug introduced by
It seems like $output defined by $this->system->cleanOutputBuffer() on line 296 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...
317
        }
318
319
        if ($willQuit) {
320
            // HHVM fix for https://github.com/facebook/hhvm/issues/4055
321 7
            $this->system->flushOutputBuffer();
322
323
            $this->system->stopExecution(1);
324 7
        }
325
326 7
        return $output;
327
    }
328
329
    /**
330
     * Converts generic PHP errors to \ErrorException
331 4
     * instances, before passing them off to be handled.
332
     *
333 7
     * This method MUST be compatible with set_error_handler.
334
     *
335 7
     * @param int    $level
336 7
     * @param string $message
337 7
     * @param string $file
338 7
     * @param int    $line
339
     *
340
     * @return bool
341
     * @throws ErrorException
342
     */
343 7
    public function handleError($level, $message, $file = null, $line = null)
344
    {
345 6
        if ($level & $this->system->getErrorReportingLevel()) {
346
            foreach ($this->silencedPatterns as $entry) {
347
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
348
                $levelMatches = $level & $entry["levels"];
349
                if ($pathMatches && $levelMatches) {
350
                    // Ignore the error, abort handling
351
                    // See https://github.com/filp/whoops/issues/418
352
                    return true;
353
                }
354
            }
355
356
            // XXX we pass $level for the "code" param only for BC reasons.
357 6
            // see https://github.com/filp/whoops/issues/267
358 6
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
359
            if ($this->canThrowExceptions) {
360 7
                throw $exception;
361
            } else {
362
                $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...
363
            }
364
            // Do not propagate errors which were already handled by Whoops.
365
            return true;
366
        }
367 7
368
        // Propagate error to the next handler, allows error_get_last() to
369
        // work on silenced errors.
370
        return false;
371
    }
372
373
    /**
374
     * Special case to deal with Fatal errors and the like.
375
     */
376
    public function handleShutdown()
377
    {
378
        // If we reached this step, we are in shutdown handler.
379
        // An exception thrown in a shutdown handler will not be propagated
380
        // to the exception handler. Pass that information along.
381
        $this->canThrowExceptions = false;
382
383
        $error = $this->system->getLastError();
384 5
        if ($error && Misc::isLevelFatal($error['type'])) {
385
            // If there was a fatal error,
386 5
            // it was not handled in handleError yet.
387 2
            $this->allowQuit = false;
388
            $this->handleError(
389
                $error['type'],
390
                $error['message'],
391
                $error['file'],
392
                $error['line']
393
            );
394
        }
395 2
    }
396
397
    /**
398
     * In certain scenarios, like in shutdown handler, we can not throw exceptions
399 2
     * @var bool
400 2
     */
401 2
    private $canThrowExceptions = true;
402
403
    private function resolveHandler($handler)
404
    {
405
        if (is_callable($handler)) {
406
            $handler = new CallbackHandler($handler);
407
        }
408
409
        if (!$handler instanceof HandlerInterface) {
410
            throw new InvalidArgumentException(
411 3
                  "Handler must be a callable, or instance of "
412
                . "Whoops\\Handler\\HandlerInterface"
413
            );
414
        }
415
416
        return $handler;
417
    }
418
419
    /**
420
     * Echo something to the browser
421
     * @param  string $output
422
     * @return $this
423
     */
424
    private function writeToOutputNow($output)
425
    {
426
        if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) {
427
            $this->system->setHttpResponseCode(
428
                $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...
429
            );
430
        }
431
432
        echo $output;
433
434
        return $this;
435
    }
436
}
437