• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / http / 22837209177

09 Mar 2026 03:32AM UTC coverage: 83.513% (-0.1%) from 83.62%
22837209177

push

github

dg
added CLAUDE.md

932 of 1116 relevant lines covered (83.51%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

46.99
/src/Http/Response.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Http;
9

10
use Nette;
11
use Nette\Utils\DateTime;
12
use function array_filter, header, header_remove, headers_list, headers_sent, htmlspecialchars, http_response_code, ini_get, is_int, ltrim, ob_get_length, ob_get_status, preg_match, rawurlencode, setcookie, str_replace, strcasecmp, strlen, strncasecmp, substr, time;
13
use const PHP_SAPI;
14

15

16
/**
17
 * Mutable HTTP response for setting status code, headers, cookies, and redirects.
18
 *
19
 * @property-read array<string,string> $headers
20
 */
21
final class Response implements IResponse
22
{
23
        use Nette\SmartObject;
24

25
        /** The domain in which the cookie will be available */
26
        public string $cookieDomain = '';
27

28
        /** The path in which the cookie will be available */
29
        public string $cookiePath = '/';
30

31
        /** Whether the cookie is available only through HTTPS */
32
        public bool $cookieSecure = false;
33

34
        /** Whether to warn when there is data in the output buffer before sending headers */
35
        public bool $warnOnBuffer = true;
36

37
        /** HTTP response code */
38
        private int $code = self::S200_OK;
39

40

41
        public function __construct()
42
        {
43
                if (is_int($code = http_response_code())) {
1✔
44
                        $this->code = $code;
×
45
                }
46
        }
1✔
47

48

49
        /**
50
         * Sets HTTP response code.
51
         * @throws Nette\InvalidArgumentException  if code is invalid
52
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
53
         */
54
        public function setCode(int $code, ?string $reason = null): static
1✔
55
        {
56
                if ($code < 100 || $code > 599) {
1✔
57
                        throw new Nette\InvalidArgumentException("Bad HTTP response '$code'.");
×
58
                }
59

60
                self::checkHeaders();
1✔
61
                $this->code = $code;
1✔
62
                $protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1';
1✔
63
                $reason ??= self::ReasonPhrases[$code] ?? 'Unknown status';
1✔
64
                header("$protocol $code $reason");
1✔
65
                return $this;
1✔
66
        }
67

68

69
        /**
70
         * Returns HTTP response code.
71
         */
72
        public function getCode(): int
73
        {
74
                return $this->code;
×
75
        }
76

77

78
        /**
79
         * Sends an HTTP header, replacing any previously sent header with the same name.
80
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
81
         */
82
        public function setHeader(string $name, ?string $value): static
1✔
83
        {
84
                self::checkHeaders();
1✔
85
                if ($value === null) {
1✔
86
                        header_remove($name);
×
87
                } elseif (strcasecmp($name, 'Content-Length') === 0 && ini_get('zlib.output_compression')) {
1✔
88
                        // ignore, PHP bug #44164
89
                } else {
90
                        header($name . ': ' . $value);
1✔
91
                }
92

93
                return $this;
1✔
94
        }
95

96

97
        /**
98
         * Adds an HTTP header without replacing a previously sent header with the same name.
99
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
100
         */
101
        public function addHeader(string $name, string $value): static
102
        {
103
                self::checkHeaders();
×
104
                header($name . ': ' . $value, replace: false);
×
105
                return $this;
×
106
        }
107

108

109
        /**
110
         * Deletes a previously sent HTTP header.
111
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
112
         */
113
        public function deleteHeader(string $name): static
114
        {
115
                self::checkHeaders();
×
116
                header_remove($name);
×
117
                return $this;
×
118
        }
119

120

121
        /**
122
         * Sends a Content-type HTTP header.
123
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
124
         */
125
        public function setContentType(string $type, ?string $charset = null): static
126
        {
127
                $this->setHeader('Content-Type', $type . ($charset ? '; charset=' . $charset : ''));
×
128
                return $this;
×
129
        }
130

131

132
        /**
133
         * Triggers a browser download dialog for the response body with the given filename.
134
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
135
         */
136
        public function sendAsFile(string $fileName): static
137
        {
138
                $this->setHeader(
×
139
                        'Content-Disposition',
×
140
                        'attachment; filename="' . str_replace('"', '', $fileName) . '"; '
×
141
                        . "filename*=utf-8''" . rawurlencode($fileName),
×
142
                );
143
                return $this;
×
144
        }
145

146

147
        /**
148
         * Redirects to another URL. Don't forget to quit the script then.
149
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
150
         */
151
        public function redirect(string $url, int $code = self::S302_Found): void
1✔
152
        {
153
                $this->setCode($code);
1✔
154
                $this->setHeader('Location', $url);
1✔
155
                if (preg_match('#^https?:|^\s*+[a-z0-9+.-]*+[^:]#i', $url)) {
1✔
156
                        $escapedUrl = htmlspecialchars($url, ENT_IGNORE | ENT_QUOTES, 'UTF-8');
1✔
157
                        echo "<h1>Redirect</h1>\n\n<p><a href=\"$escapedUrl\">Please click here to continue</a>.</p>";
1✔
158
                }
159
        }
1✔
160

161

162
        /**
163
         * Sets the Cache-Control and Expires headers. Pass a time string (e.g. '20 minutes') to enable caching,
164
         * or null to disable it entirely.
165
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
166
         */
167
        public function setExpiration(?string $expire): static
168
        {
169
                $this->setHeader('Pragma', null);
×
170
                if (!$expire) { // no cache
×
171
                        $this->setHeader('Cache-Control', 's-maxage=0, max-age=0, must-revalidate');
×
172
                        $this->setHeader('Expires', 'Mon, 23 Jan 1978 10:00:00 GMT');
×
173
                        return $this;
×
174
                }
175

176
                $expire = DateTime::from($expire);
×
177
                $this->setHeader('Cache-Control', 'max-age=' . ($expire->format('U') - time()));
×
178
                $this->setHeader('Expires', Helpers::formatDate($expire));
×
179
                return $this;
×
180
        }
181

182

183
        /**
184
         * Checks whether HTTP headers have already been sent, making it impossible to modify them.
185
         */
186
        public function isSent(): bool
187
        {
188
                return headers_sent();
1✔
189
        }
190

191

192
        /**
193
         * Returns the sent HTTP header, or `null` if it does not exist. The parameter is case-insensitive.
194
         */
195
        public function getHeader(string $header): ?string
196
        {
197
                $header .= ':';
×
198
                $len = strlen($header);
×
199
                foreach (headers_list() as $item) {
×
200
                        if (strncasecmp($item, $header, $len) === 0) {
×
201
                                return ltrim(substr($item, $len));
×
202
                        }
203
                }
204

205
                return null;
×
206
        }
207

208

209
        /**
210
         * Returns all sent HTTP headers as associative array.
211
         * @return array<string, string>
212
         */
213
        public function getHeaders(): array
214
        {
215
                $headers = [];
×
216
                foreach (headers_list() as $header) {
×
217
                        $parts = explode(':', $header, 2);
×
218
                        if (isset($parts[1])) {
×
219
                                $headers[$parts[0]] = ltrim($parts[1]);
×
220
                        }
221
                }
222

223
                return $headers;
×
224
        }
225

226

227
        /**
228
         * Sends a cookie.
229
         * @param self::SameSite*|null  $sameSite
230
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
231
         */
232
        public function setCookie(
1✔
233
                string $name,
234
                string $value,
235
                string|int|\DateTimeInterface|null $expire,
236
                ?string $path = null,
237
                ?string $domain = null,
238
                ?bool $secure = null,
239
                ?bool $httpOnly = null,
240
                ?string $sameSite = null,
241
        ): static
242
        {
243
                self::checkHeaders();
1✔
244
                setcookie($name, $value, [
1✔
245
                        'expires' => $expire ? (int) DateTime::from($expire)->format('U') : 0,
1✔
246
                        'path' => $path ?? ($domain ? '/' : $this->cookiePath),
1✔
247
                        'domain' => $domain ?? ($path ? '' : $this->cookieDomain),
1✔
248
                        'secure' => $secure ?? $this->cookieSecure,
1✔
249
                        'httponly' => $httpOnly ?? true,
1✔
250
                        'samesite' => $sameSite ?? self::SameSiteLax,
1✔
251
                ]);
252
                return $this;
1✔
253
        }
254

255

256
        /**
257
         * Deletes a cookie.
258
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
259
         */
260
        public function deleteCookie(
1✔
261
                string $name,
262
                ?string $path = null,
263
                ?string $domain = null,
264
                ?bool $secure = null,
265
        ): void
266
        {
267
                $this->setCookie($name, '', 0, $path, $domain, $secure);
1✔
268
        }
1✔
269

270

271
        private function checkHeaders(): void
272
        {
273
                if (PHP_SAPI === 'cli') {
1✔
274
                } elseif (headers_sent($file, $line)) {
×
275
                        throw new Nette\InvalidStateException('Cannot send header after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.'));
×
276

277
                } elseif (
278
                        $this->warnOnBuffer &&
×
279
                        ob_get_length() &&
×
280
                        !array_filter(ob_get_status(full_status: true), fn(array $i): bool => !$i['chunk_size'])
×
281
                ) {
282
                        trigger_error('Possible problem: you are sending a HTTP header while already having some data in output buffer. Try Tracy\OutputDebugger or send cookies/start session earlier.');
×
283
                }
284
        }
1✔
285
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc