Completed
Pull Request — master (#600)
by Filip
38:24
created

Run::prependHandler()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 16

Duplication

Lines 16
Ratio 100 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
dl 16
loc 16
ccs 3
cts 3
cp 1
rs 9.7333
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3
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 View Code Duplication
    public function pushHandler($handler)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
     * Prepends a handler to the begining of the stack
69
     *
70
     * @throws InvalidArgumentException  If argument is not callable or instance of HandlerInterface
71
     * @param  Callable|HandlerInterface $handler
72 1
     * @return Run
73
     */
74 1 View Code Duplication
    public function prependHandler($handler)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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