Completed
Push — master ( f4a3d8...a5083c )
by Denis
08:21 queued 05:50
created

Run::sendHttpCode()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 22
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 6.0163

Importance

Changes 0
Metric Value
dl 0
loc 22
c 0
b 0
f 0
ccs 12
cts 13
cp 0.9231
rs 8.6737
cc 6
eloc 11
nc 6
nop 1
crap 6.0163
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
    public function pushHandler($handler)
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
     * 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 1
    private function getInspector($exception)
103
    {
104 1
        return new Inspector($exception);
105
    }
106
107
    /**
108
     * Registers this instance as an error handler.
109
     * @return Run
110
     */
111 4
    public function register()
112
    {
113 4
        if (!$this->isRegistered) {
114
            // Workaround PHP bug 42098
115
            // https://bugs.php.net/bug.php?id=42098
116 4
            class_exists("\\Whoops\\Exception\\ErrorException");
117 4
            class_exists("\\Whoops\\Exception\\FrameCollection");
118 4
            class_exists("\\Whoops\\Exception\\Frame");
119 4
            class_exists("\\Whoops\\Exception\\Inspector");
120
121 4
            $this->system->setErrorHandler([$this, self::ERROR_HANDLER]);
122 4
            $this->system->setExceptionHandler([$this, self::EXCEPTION_HANDLER]);
123 4
            $this->system->registerShutdownFunction([$this, self::SHUTDOWN_HANDLER]);
124
125 4
            $this->isRegistered = true;
126 4
        }
127
128 4
        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 4
    public function allowQuit($exit = null)
153
    {
154 4
        if (func_num_args() == 0) {
155 1
            return $this->allowQuit;
156
        }
157
158 4
        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 3
    public function sendHttpCode($code = null)
204
    {
205 3
        if (func_num_args() == 0) {
206 2
            return $this->sendHttpCode;
207
        }
208
209 2
        if (!$code) {
210
            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 3
    public function writeToOutput($send = null)
233
    {
234 3
        if (func_num_args() == 0) {
235 3
            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 5
    public function handleException($exception)
249
    {
250
        // Walk the registered handlers in the reverse order
251
        // they were registered, and pass off the exception
252 5
        $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 5
        $this->system->startOutputBuffering();
258
259
        // Just in case there are no handlers:
260 5
        $handlerResponse = null;
261
262 5
        foreach (array_reverse($this->handlerStack) as $handler) {
263 5
            $handler->setRun($this);
264 5
            $handler->setInspector($inspector);
265 5
            $handler->setException($exception);
266
267
            // The HandlerInterface does not require an Exception passed to handle()
268
            // and neither of our bundled handlers use it.
269
            // However, 3rd party handlers may have already relied on this parameter,
270
            // and removing it would be possibly breaking for users.
271 5
            $handlerResponse = $handler->handle($exception);
272
273 5
            if (in_array($handlerResponse, [Handler::LAST_HANDLER, Handler::QUIT])) {
274
                // The Handler has handled the exception in some way, and
275
                // wishes to quit execution (Handler::QUIT), or skip any
276
                // other handlers (Handler::LAST_HANDLER). If $this->allowQuit
277
                // is false, Handler::QUIT behaves like Handler::LAST_HANDLER
278 2
                break;
279
            }
280 5
        }
281
282 5
        $willQuit = $handlerResponse == Handler::QUIT && $this->allowQuit();
283
284 5
        $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 309 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...
285
286
        // If we're allowed to, send output generated by handlers directly
287
        // to the output, otherwise, and if the script doesn't quit, return
288
        // it so that it may be used by the caller
289 5
        if ($this->writeToOutput()) {
290
            // @todo Might be able to clean this up a bit better
291
            // If we're going to quit execution, cleanup all other output
292
            // buffers before sending our own output:
293 4
            if ($willQuit) {
294
                while ($this->system->getOutputBufferLevel() > 0) {
295
                    $this->system->endOutputBuffering();
296
                }
297
            }
298
299 4
            $this->writeToOutputNow($output);
0 ignored issues
show
Security Bug introduced by
It seems like $output defined by $this->system->cleanOutputBuffer() on line 284 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...
300 4
        }
301
302 5
        if ($willQuit) {
303
            // HHVM fix for https://github.com/facebook/hhvm/issues/4055
304
            $this->system->flushOutputBuffer();
305
306
            $this->system->stopExecution(1);
307
        }
308
309 5
        return $output;
310
    }
311
312
    /**
313
     * Converts generic PHP errors to \ErrorException
314
     * instances, before passing them off to be handled.
315
     *
316
     * This method MUST be compatible with set_error_handler.
317
     *
318
     * @param int    $level
319
     * @param string $message
320
     * @param string $file
321
     * @param int    $line
322
     *
323
     * @return bool
324
     * @throws ErrorException
325
     */
326 5
    public function handleError($level, $message, $file = null, $line = null)
327
    {
328 5
        if ($level & $this->system->getErrorReportingLevel()) {
329 2
            foreach ($this->silencedPatterns as $entry) {
330
                $pathMatches = (bool) preg_match($entry["pattern"], $file);
331
                $levelMatches = $level & $entry["levels"];
332
                if ($pathMatches && $levelMatches) {
333
                    // Ignore the error, abort handling
334
                    return true;
335
                }
336 2
            }
337
338
            // XXX we pass $level for the "code" param only for BC reasons.
339
            // see https://github.com/filp/whoops/issues/267
340 2
            $exception = new ErrorException($message, /*code*/ $level, /*severity*/ $level, $file, $line);
341 2
            if ($this->canThrowExceptions) {
342 2
                throw $exception;
343
            } else {
344
                $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...
345
            }
346
            // Do not propagate errors which were already handled by Whoops.
347
            return true;
348
        }
349
350
        // Propagate error to the next handler, allows error_get_last() to
351
        // work on silenced errors.
352 3
        return false;
353
    }
354
355
    /**
356
     * Special case to deal with Fatal errors and the like.
357
     */
358
    public function handleShutdown()
359
    {
360
        // If we reached this step, we are in shutdown handler.
361
        // An exception thrown in a shutdown handler will not be propagated
362
        // to the exception handler. Pass that information along.
363
        $this->canThrowExceptions = false;
364
365
        $error = $this->system->getLastError();
366
        if ($error && Misc::isLevelFatal($error['type'])) {
367
            // If there was a fatal error,
368
            // it was not handled in handleError yet.
369
            $this->handleError(
370
                $error['type'],
371
                $error['message'],
372
                $error['file'],
373
                $error['line']
374
            );
375
        }
376
    }
377
378
    /**
379
     * In certain scenarios, like in shutdown handler, we can not throw exceptions
380
     * @var bool
381
     */
382
    private $canThrowExceptions = true;
383
384
    /**
385
     * Echo something to the browser
386
     * @param  string $output
387
     * @return $this
388
     */
389 1
    private function writeToOutputNow($output)
390
    {
391 1
        if ($this->sendHttpCode() && \Whoops\Util\Misc::canSendHeaders()) {
392
            $this->system->setHttpResponseCode(
393
                $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...
394
            );
395
        }
396
397 1
        echo $output;
398
399 1
        return $this;
400
    }
401
}
402