Completed
Push — master ( 6b4c19...4c97f8 )
by Denis
01:14
created

Run::removeFirstHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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
     * See removeFirstHandler and removeLastHandler
76 6
     * @return null|HandlerInterface
77
     */
78 6
    public function popHandler()
79 6
    {
80
        return array_pop($this->handlerStack);
81
    }
82
83
84
    /**
85
     * Removes the first handler
86
     */
87
    public function removeFirstHandler()
88
    {
89 6
        array_pop($this->handlerStack);
90
    }
91 6
92
    /**
93
     * Removes the last handler
94
     */
95 6
    public function removeLastHandler()
96
    {
97
        array_shift($this->handlerStack);
98
    }
99
100
    /**
101
     * Returns an array with all handlers, in the
102 6
     * order they were added to the stack.
103
     * @return array
104
     */
105
    public function getHandlers()
106
    {
107
        return $this->handlerStack;
108
    }
109
110 1
    /**
111
     * Clears all handlers in the handlerStack, including
112 1
     * the default PrettyPage handler.
113
     * @return Run
114
     */
115
    public function clearHandlers()
116
    {
117
        $this->handlerStack = [];
118
        return $this;
119
    }
120
121
    /**
122
     * @param  \Throwable $exception
123
     * @return Inspector
124
     */
125
    private function getInspector($exception)
126
    {
127
        return new Inspector($exception);
128
    }
129
130 2
    /**
131
     * Registers this instance as an error handler.
132 2
     * @return Run
133
     */
134
    public function register()
135
    {
136
        if (!$this->isRegistered) {
137
            // Workaround PHP bug 42098
138
            // https://bugs.php.net/bug.php?id=42098
139
            class_exists("\\Whoops\\Exception\\ErrorException");
140 1
            class_exists("\\Whoops\\Exception\\FrameCollection");
141
            class_exists("\\Whoops\\Exception\\Frame");
142 1
            class_exists("\\Whoops\\Exception\\Inspector");
143 1
144
            $this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
145
            $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
146
            $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
147
148
            $this->isRegistered = true;
149
        }
150 3
151
        return $this;
152 3
    }
153
154
    /**
155
     * Unregisters all handlers registered by this Whoops\Run instance
156
     * @return Run
157
     */
158
    public function unregister()
159 6
    {
160
        if ($this->isRegistered) {
161 6
            $this->system->restoreExceptionHandler();
162
            $this->system->restoreErrorHandler();
163
164 6
            $this->isRegistered = false;
165 6
        }
166 6
167 6
        return $this;
168
    }
169 6
170 6
    /**
171 6
     * Should Whoops allow Handlers to force the script to quit?
172
     * @param  bool|int $exit
173 6
     * @return bool
174 6
     */
175
    public function allowQuit($exit = null)
176 6
    {
177
        if (func_num_args() == 0) {
178
            return $this->allowQuit;
179
        }
180
181
        return $this->allowQuit = (bool) $exit;
182
    }
183 1
184
    /**
185 1
     * Silence particular errors in particular files
186 1
     * @param  array|string $patterns List or a single regex pattern to match
187 1
     * @param  int          $levels   Defaults to E_STRICT | E_DEPRECATED
188
     * @return \Whoops\Run
189 1
     */
190 1
    public function silenceErrorsInPaths($patterns, $levels = 10240)
191
    {
192 1
        $this->silencedPatterns = array_merge(
193
            $this->silencedPatterns,
194
            array_map(
195
                function ($pattern) use ($levels) {
196
                    return [
197
                        "pattern" => $pattern,
198
                        "levels" => $levels,
199
                    ];
200 6
                },
201
                (array) $patterns
202 6
            )
203 3
        );
204
        return $this;
205
    }
206 6
207
208
    /**
209
     * Returns an array with silent errors in path configuration
210
     *
211
     * @return array
212
     */
213
    public function getSilenceErrorsInPaths()
214
    {
215 1
        return $this->silencedPatterns;
216
    }
217 1
218 1
    /*
219 1
     * Should Whoops send HTTP error code to the browser if possible?
220 1
     * Whoops will by default send HTTP code 500, but you may wish to
221
     * use 502, 503, or another 5xx family code.
222 1
     *
223 1
     * @param bool|int $code
224 1
     * @return int|false
225 1
     */
226
    public function sendHttpCode($code = null)
227 1
    {
228 1
        if (func_num_args() == 0) {
229 1
            return $this->sendHttpCode;
230
        }
231
232
        if (!$code) {
233
            return $this->sendHttpCode = false;
234
        }
235
236
        if ($code === true) {
237
            $code = 500;
238
        }
239
240
        if ($code < 400 || 600 <= $code) {
241
            throw new InvalidArgumentException(
242
                 "Invalid status code '$code', must be 4xx or 5xx"
243
            );
244
        }
245
246
        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 246 which is incompatible with the return type declared by the interface Whoops\RunInterface::sendHttpCode of type integer|false.
Loading history...
247
    }
248
249
    /**
250
     * Should Whoops push output directly to the client?
251 6
     * If this is false, output will be returned by handleException
252
     * @param  bool|int $send
253 6
     * @return bool
254 4
     */
255
    public function writeToOutput($send = null)
256
    {
257 3
        if (func_num_args() == 0) {
258 1
            return $this->sendOutput;
259
        }
260
261 2
        return $this->sendOutput = (bool) $send;
262 1
    }
263 1
264
    /**
265 2
     * Handles an exception, ultimately generating a Whoops error
266 1
     * page.
267 1
     *
268 1
     * @param  \Throwable $exception
269
     * @return string     Output generated by handlers
270
     */
271 1
    public function handleException($exception)
272
    {
273
        // Walk the registered handlers in the reverse order
274
        // they were registered, and pass off the exception
275
        $inspector = $this->getInspector($exception);
276
277
        // Capture output produced while handling the exception,
278
        // we might want to send it straight away to the client,
279
        // or return it silently.
280 5
        $this->system->startOutputBuffering();
281
282 5
        // Just in case there are no handlers:
283 5
        $handlerResponse = null;
284
        $handlerContentType = null;
285
286 1
        try {
287
            foreach (array_reverse($this->handlerStack) as $handler) {
288
                $handler->setRun($this);
289
                $handler->setInspector($inspector);
290
                $handler->setException($exception);
291
292
                // The HandlerInterface does not require an Exception passed to handle()
293
                // and neither of our bundled handlers use it.
294
                // However, 3rd party handlers may have already relied on this parameter,
295
                // and removing it would be possibly breaking for users.
296 7
                $handlerResponse = $handler->handle($exception);
297
298
                // Collect the content type for possible sending in the headers.
299
                $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...
300 7
301
                if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
302
                    // The Handler has handled the exception in some way, and
303
                    // wishes to quit execution (Handler::QUIT), or skip any
304
                    // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
305 7
                    // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
306
                    break;
307
                }
308 7
            }
309 7
310
            $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
311
        } finally {
312 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 342 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...
313 7
        }
314 7
315 7
        // If we're allowed to, send output generated by handlers directly
316
        // to the output, otherwise, and if the script doesn't quit, return
317
        // it so that it may be used by the caller
318
        if ($this->writeToOutput()) {
319
            // @todo Might be able to clean this up a bit better
320
            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...
321 7
                // Cleanup all other output buffers before sending our output:
322
                while ($this->system->getOutputBufferLevel() > 0) {
323
                    $this->system->endOutputBuffering();
324 7
                }
325
326 7
                // Send any headers if needed:
327
                if (Misc::canSendHeaders() && $handlerContentType) {
328
                    header("Content-Type: {$handlerContentType}");
329
                }
330
            }
331 4
332
            $this->writeToOutputNow($output);
0 ignored issues
show
Security Bug introduced by
It seems like $output defined by $this->system->cleanOutputBuffer() on line 312 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...
333 7
        }
334
335 7
        if ($willQuit) {
336 7
            // HHVM fix for https://github.com/facebook/hhvm/issues/4055
337 7
            $this->system->flushOutputBuffer();
338 7
339
            $this->system->stopExecution(1);
340
        }
341
342
        return $output;
343 7
    }
344
345 6
    /**
346
     * Converts generic PHP errors to \ErrorException
347
     * instances, before passing them off to be handled.
348
     *
349
     * This method MUST be compatible with set_error_handler.
350
     *
351
     * @param int    $level
352
     * @param string $message
353
     * @param string $file
354
     * @param int    $line
355
     *
356
     * @return bool
357 6
     * @throws ErrorException
358 6
     */
359
    public function handleError($level, $message, $file = null, $line = null)
360 7
    {
361
        if ($level & $this->system->getErrorReportingLevel()) {
362
            foreach ($this->silencedPatterns as $entry) {
363
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
364
                $levelMatches = $level & $entry["levels"];
365
                if ($pathMatches && $levelMatches) {
366
                    // Ignore the error, abort handling
367 7
                    // See https://github.com/filp/whoops/issues/418
368
                    return true;
369
                }
370
            }
371
372
            // XXX we pass $level for the "code" param only for BC reasons.
373
            // see https://github.com/filp/whoops/issues/267
374
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
375
            if ($this->canThrowExceptions) {
376
                throw $exception;
377
            } else {
378
                $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...
379
            }
380
            // Do not propagate errors which were already handled by Whoops.
381
            return true;
382
        }
383
384 5
        // Propagate error to the next handler, allows error_get_last() to
385
        // work on silenced errors.
386 5
        return false;
387 2
    }
388
389
    /**
390
     * Special case to deal with Fatal errors and the like.
391
     */
392
    public function handleShutdown()
393
    {
394
        // If we reached this step, we are in shutdown handler.
395 2
        // An exception thrown in a shutdown handler will not be propagated
396
        // to the exception handler. Pass that information along.
397
        $this->canThrowExceptions = false;
398
399 2
        $error = $this->system->getLastError();
400 2
        if ($error && Misc::isLevelFatal($error['type'])) {
401 2
            // If there was a fatal error,
402
            // it was not handled in handleError yet.
403
            $this->allowQuit = false;
404
            $this->handleError(
405
                $error['type'],
406
                $error['message'],
407
                $error['file'],
408
                $error['line']
409
            );
410
        }
411 3
    }
412
413
    /**
414
     * In certain scenarios, like in shutdown handler, we can not throw exceptions
415
     * @var bool
416
     */
417
    private $canThrowExceptions = true;
418
419
    private function resolveHandler($handler)
420
    {
421
        if (is_callable($handler)) {
422
            $handler = new CallbackHandler($handler);
423
        }
424
425
        if (!$handler instanceof HandlerInterface) {
426
            throw new InvalidArgumentException(
427
                  "Handler must be a callable, or instance of "
428
                . "Whoops\\Handler\\HandlerInterface"
429
            );
430
        }
431
432
        return $handler;
433
    }
434
435
    /**
436
     * Echo something to the browser
437
     * @param  string $output
438
     * @return $this
439
     */
440
    private function writeToOutputNow($output)
441
    {
442
        if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) {
443
            $this->system->setHttpResponseCode(
444
                $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...
445
            );
446
        }
447
448
        echo $output;
449 3
450
        return $this;
451 3
    }
452
}
453