Completed
Pull Request — master (#631)
by Thomas
01:42
created

Run::prependHandler()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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