Issues (1)

src/SapiEmitter.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace HttpSoft\Emitter;
6
7
use HttpSoft\Emitter\Exception\HeadersAlreadySentException;
8
use HttpSoft\Emitter\Exception\OutputAlreadySentException;
9
use InvalidArgumentException;
10
use Psr\Http\Message\ResponseInterface;
11
use Psr\Http\Message\StreamInterface;
12
13
use function flush;
14
use function ob_get_length;
15
use function ob_get_level;
16
use function preg_match;
17
use function sprintf;
18
use function strlen;
19
use function str_replace;
20
use function strtolower;
21
use function trim;
22
use function ucwords;
23
24
final class SapiEmitter implements EmitterInterface
25
{
26
    /**
27
     * @var int|null
28
     */
29
    private ?int $bufferLength;
30
31
    /**
32
     * @param int|null $bufferLength
33
     * @throws InvalidArgumentException if buffer length is integer type and less than or one.
34
     */
35 12
    public function __construct(?int $bufferLength = null)
36
    {
37 12
        if ($bufferLength !== null && $bufferLength < 1) {
38 1
            throw new InvalidArgumentException(sprintf(
39 1
                'Buffer length for `%s` must be greater than zero; received `%d`.',
40 1
                self::class,
41 1
                $bufferLength
42 1
            ));
43
        }
44
45 11
        $this->bufferLength = $bufferLength;
46
    }
47
48
    /**
49
     * {@inheritDoc}
50
     *
51
     * @throws HeadersAlreadySentException if headers already sent.
52
     * @throws OutputAlreadySentException if output has been emitted previously.
53
     */
54 11
    public function emit(ResponseInterface $response, bool $withoutBody = false): void
55
    {
56 11
        if (headers_sent()) {
57 1
            throw HeadersAlreadySentException::create();
58
        }
59
60 10
        if (ob_get_level() > 0 && ob_get_length() > 0) {
61 1
            throw OutputAlreadySentException::create();
62
        }
63
64 9
        $this->emitHeaders($response);
65 9
        $this->emitStatusLine($response);
66
67 9
        if (!$withoutBody && $response->getBody()->isReadable()) {
68 6
            $this->emitBody($response);
69
        }
70
    }
71
72
    /**
73
     * Loops through and emits each header as specified to `Psr\Http\Message\MessageInterface::getHeaders()`.
74
     *
75
     * @param ResponseInterface $response
76
     */
77 9
    private function emitHeaders(ResponseInterface $response): void
78
    {
79 9
        foreach ($response->getHeaders() as $name => $values) {
80 7
            $name = str_replace(' ', '-', ucwords(strtolower(str_replace('-', ' ', (string) $name))));
81 7
            $firstReplace = !($name === 'Set-Cookie');
82
83 7
            foreach ($values as $value) {
84 7
                header("$name: $value", $firstReplace);
85 7
                $firstReplace = false;
86
            }
87
        }
88
    }
89
90
    /**
91
     * Emits the response status line.
92
     *
93
     * @param ResponseInterface $response
94
     * @psalm-suppress RedundantCast
95
     */
96 9
    private function emitStatusLine(ResponseInterface $response): void
97
    {
98 9
        $statusCode = (int) $response->getStatusCode();
99 9
        $reasonPhrase = trim((string) $response->getReasonPhrase());
100 9
        $protocolVersion = trim((string) $response->getProtocolVersion());
101
102 9
        $status = $statusCode . (!$reasonPhrase ? '' : " $reasonPhrase");
103 9
        header("HTTP/$protocolVersion $status", true, $statusCode);
104
    }
105
106
    /**
107
     * Emits the message body.
108
     *
109
     * @param ResponseInterface $response
110
     * @psalm-suppress MixedArgument
111
     */
112 6
    private function emitBody(ResponseInterface $response): void
113
    {
114 6
        if ($this->bufferLength === null) {
115 3
            echo $response->getBody();
116 3
            return;
117
        }
118
119 3
        flush();
120 3
        $body = $response->getBody();
121 3
        $range = $this->parseContentRange($response->getHeaderLine('Content-Range'));
122
123 3
        if ($range !== null && isset($range['unit']) && $range['unit'] === 'bytes') {
124 1
            $this->emitBodyRange($body, $range['first'], $range['last']);
125 1
            return;
126
        }
127
128 2
        if ($body->isSeekable()) {
129 2
            $body->rewind();
130
        }
131
132 2
        while (!$body->eof()) {
133 2
            echo $body->read($this->bufferLength);
134
        }
135
    }
136
137
    /**
138
     * Emits a range of the message body.
139
     *
140
     * @param StreamInterface $body
141
     * @param int $first
142
     * @param int $last
143
     * @psalm-suppress PossiblyNullArgument
144
     */
145 1
    private function emitBodyRange(StreamInterface $body, int $first, int $last): void
146
    {
147 1
        $length = $last - $first + 1;
148
149 1
        if ($body->isSeekable()) {
150 1
            $body->seek($first);
151
        }
152
153 1
        while ($length >= $this->bufferLength && !$body->eof()) {
154 1
            $contents = $body->read($this->bufferLength);
0 ignored issues
show
It seems like $this->bufferLength can also be of type null; however, parameter $length of Psr\Http\Message\StreamInterface::read() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

154
            $contents = $body->read(/** @scrutinizer ignore-type */ $this->bufferLength);
Loading history...
155 1
            $length -= strlen($contents);
156 1
            echo $contents;
157
        }
158
159 1
        if ($length > 0 && !$body->eof()) {
160
            echo $body->read($length);
161
        }
162
    }
163
164
    /**
165
     * Parse Content-Range header.
166
     *
167
     * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.16
168
     * @param string $header
169
     * @return array|null
170
     */
171 3
    private function parseContentRange(string $header): ?array
172
    {
173 3
        if (empty($header)) {
174 2
            return null;
175
        }
176
177 1
        if (preg_match('/(?P<unit>\w+)\s+(?P<first>\d+)-(?P<last>\d+)\/(?P<length>\d+|\*)/', $header, $matches)) {
178 1
            return [
179 1
                'unit' => $matches['unit'],
180 1
                'first' => (int) $matches['first'],
181 1
                'last' => (int) $matches['last'],
182 1
                'length' => ($matches['length'] === '*') ? '*' : (int) $matches['length'],
183 1
            ];
184
        }
185
186
        return null;
187
    }
188
}
189