Completed
Push — master ( 114a8f...51c2de )
by Denis
01:38
created

Run::writeToOutputNow()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4.125

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 4
cts 8
cp 0.5
rs 9.8666
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 4.125
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
     * 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 10
    public function pushHandler($handler)
51
    {
52 10
        if (is_callable($handler)) {
53 1
            $handler = new CallbackHandler($handler);
54 1
        }
55
56 10
        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 9
        $this->handlerStack[] = $handler;
64 9
        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 3
    private function getInspector($exception)
103
    {
104 3
        return new Inspector($exception);
105
    }
106
107
    /**
108
     * Registers this instance as an error handler.
109
     * @return Run
110
     */
111 6
    public function register()
112
    {
113 6
        if (!$this->isRegistered) {
114
            // Workaround PHP bug 42098
115
            // https://bugs.php.net/bug.php?id=42098
116 6
            class_exists("\\Whoops\\Exception\\ErrorException");
117 6
            class_exists("\\Whoops\\Exception\\FrameCollection");
118 6
            class_exists("\\Whoops\\Exception\\Frame");
119 6
            class_exists("\\Whoops\\Exception\\Inspector");
120
121 6
            $this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
122 6
            $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
123 6
            $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
124
125 6
            $this->isRegistered = true;
126 6
        }
127
128 6
        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 6
    public function allowQuit($exit = null)
153
    {
154 6
        if (func_num_args() == 0) {
155 3
            return $this->allowQuit;
156
        }
157
158 6
        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
    /**
186
     * Returns an array with silent errors in path configuration
187
     *
188
     * @return array
189
     */
190
    public function getSilenceErrorsInPaths()
191
    {
192
        return $this->silencedPatterns;
193
    }
194
195
    /*
196
     * Should Whoops send HTTP error code to the browser if possible?
197
     * Whoops will by default send HTTP code 500, but you may wish to
198
     * use 502, 503, or another 5xx family code.
199
     *
200
     * @param bool|int $code
201
     * @return int|false
202
     */
203 6
    public function sendHttpCode($code = null)
204
    {
205 6
        if (func_num_args() == 0) {
206 4
            return $this->sendHttpCode;
207
        }
208
209 3
        if (!$code) {
210 1
            return $this->sendHttpCode = false;
211
        }
212
213 2
        if ($code === true) {
214 1
            $code = 500;
215 1
        }
216
217 2
        if ($code < 400 || 600 <= $code) {
218 1
            throw new InvalidArgumentException(
219 1
                 "Invalid status code '$code', must be 4xx or 5xx"
220 1
            );
221
        }
222
223 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 223 which is incompatible with the return type declared by the interface Whoops\RunInterface::sendHttpCode of type integer|false.
Loading history...
224
    }
225
226
    /**
227
     * Should Whoops push output directly to the client?
228
     * If this is false, output will be returned by handleException
229
     * @param  bool|int $send
230
     * @return bool
231
     */
232 5
    public function writeToOutput($send = null)
233
    {
234 5
        if (func_num_args() == 0) {
235 5
            return $this->sendOutput;
236
        }
237
238 1
        return $this->sendOutput = (bool) $send;
239
    }
240
241
    /**
242
     * Handles an exception, ultimately generating a Whoops error
243
     * page.
244
     *
245
     * @param  \Throwable $exception
246
     * @return string     Output generated by handlers
247
     */
248 7
    public function handleException($exception)
249
    {
250
        // Walk the registered handlers in the reverse order
251
        // they were registered, and pass off the exception
252 7
        $inspector = $this->getInspector($exception);
253
254
        // Capture output produced while handling the exception,
255
        // we might want to send it straight away to the client,
256
        // or return it silently.
257 7
        $this->system->startOutputBuffering();
258
259
        // Just in case there are no handlers:
260 7
        $handlerResponse = null;
261 7
        $handlerContentType = null;
262
263 7
        foreach (array_reverse($this->handlerStack) as $handler) {
264 7
            $handler->setRun($this);
265 7
            $handler->setInspector($inspector);
266 7
            $handler->setException($exception);
267
268
            // The HandlerInterface does not require an Exception passed to handle()
269
            // and neither of our bundled handlers use it.
270
            // However, 3rd party handlers may have already relied on this parameter,
271
            // and removing it would be possibly breaking for users.
272 7
            $handlerResponse = $handler->handle($exception);
273
274
            // Collect the content type for possible sending in the headers.
275 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...
276
277 7
            if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
278
                // The Handler has handled the exception in some way, and
279
                // wishes to quit execution (Handler::QUIT), or skip any
280
                // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
281
                // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
282 4
                break;
283
            }
284 7
        }
285
286 7
        $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
287
288 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 317 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...
289
290
        // If we're allowed to, send output generated by handlers directly
291
        // to the output, otherwise, and if the script doesn't quit, return
292
        // it so that it may be used by the caller
293 7
        if ($this->writeToOutput()) {
294
            // @todo Might be able to clean this up a bit better
295 6
            if ($willQuit) {
296
                // Cleanup all other output buffers before sending our output:
297
                while ($this->system->getOutputBufferLevel() > 0) {
298
                    $this->system->endOutputBuffering();
299
                }
300
301
                // Send any headers if needed:
302
                if (Misc::canSendHeaders() && $handlerContentType) {
303
                    header("Content-Type: {$handlerContentType}");
304
                }
305
            }
306
307 6
            $this->writeToOutputNow($output);
0 ignored issues
show
Security Bug introduced by
It seems like $output defined by $this->system->cleanOutputBuffer() on line 288 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...
308 6
        }
309
310 7
        if ($willQuit) {
311
            // HHVM fix for https://github.com/facebook/hhvm/issues/4055
312
            $this->system->flushOutputBuffer();
313
314
            $this->system->stopExecution(1);
315
        }
316
317 7
        return $output;
318
    }
319
320
    /**
321
     * Converts generic PHP errors to \ErrorException
322
     * instances, before passing them off to be handled.
323
     *
324
     * This method MUST be compatible with set_error_handler.
325
     *
326
     * @param int    $level
327
     * @param string $message
328
     * @param string $file
329
     * @param int    $line
330
     *
331
     * @return bool
332
     * @throws ErrorException
333
     */
334 5
    public function handleError($level, $message, $file = null, $line = null)
335
    {
336 5
        if ($level & $this->system->getErrorReportingLevel()) {
337 2
            foreach ($this->silencedPatterns as $entry) {
338
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
339
                $levelMatches = $level & $entry["levels"];
340
                if ($pathMatches && $levelMatches) {
341
                    // Ignore the error, abort handling
342
                    // See https://github.com/filp/whoops/issues/418
343
                    return true;
344
                }
345 2
            }
346
347
            // XXX we pass $level for the "code" param only for BC reasons.
348
            // see https://github.com/filp/whoops/issues/267
349 2
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
350 2
            if ($this->canThrowExceptions) {
351 2
                throw $exception;
352
            } else {
353
                $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...
354
            }
355
            // Do not propagate errors which were already handled by Whoops.
356
            return true;
357
        }
358
359
        // Propagate error to the next handler, allows error_get_last() to
360
        // work on silenced errors.
361 3
        return false;
362
    }
363
364
    /**
365
     * Special case to deal with Fatal errors and the like.
366
     */
367
    public function handleShutdown()
368
    {
369
        // If we reached this step, we are in shutdown handler.
370
        // An exception thrown in a shutdown handler will not be propagated
371
        // to the exception handler. Pass that information along.
372
        $this->canThrowExceptions = false;
373
374
        $error = $this->system->getLastError();
375
        if ($error && Misc::isLevelFatal($error['type'])) {
376
            // If there was a fatal error,
377
            // it was not handled in handleError yet.
378
            $this->allowQuit = false;
379
            $this->handleError(
380
                $error['type'],
381
                $error['message'],
382
                $error['file'],
383
                $error['line']
384
            );
385
        }
386
    }
387
388
    /**
389
     * In certain scenarios, like in shutdown handler, we can not throw exceptions
390
     * @var bool
391
     */
392
    private $canThrowExceptions = true;
393
394
    /**
395
     * Echo something to the browser
396
     * @param  string $output
397
     * @return $this
398
     */
399 3
    private function writeToOutputNow($output)
400
    {
401 3
        if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) {
402
            $this->system->setHttpResponseCode(
403
                $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...
404
            );
405
        }
406
407 3
        echo $output;
408
409 3
        return $this;
410
    }
411
}
412