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

nette / http / 21832799728

09 Feb 2026 04:17PM UTC coverage: 83.62% (-0.2%) from 83.772%
21832799728

push

github

dg
Request::isSameSite() is silently deprecated

924 of 1105 relevant lines covered (83.62%)

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
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
declare(strict_types=1);
9

10
namespace Nette\Http;
11

12
use Nette;
13
use Nette\Utils\DateTime;
14
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;
15
use const ENT_IGNORE, ENT_QUOTES, PHP_SAPI;
16

17

18
/**
19
 * HttpResponse class.
20
 *
21
 * @property-read array<string,string> $headers
22
 */
23
final class Response implements IResponse
24
{
25
        use Nette\SmartObject;
26

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

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

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

36
        /** Whether warn on possible problem with data in output buffer */
37
        public bool $warnOnBuffer = true;
38

39
        /** HTTP response code */
40
        private int $code = self::S200_OK;
41

42

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

50

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

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

70

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

79

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

95
                return $this;
1✔
96
        }
97

98

99
        /**
100
         * Sends an HTTP header and doesn't overwrite previously sent header of the same name.
101
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
102
         */
103
        public function addHeader(string $name, string $value): static
104
        {
105
                self::checkHeaders();
×
106
                header($name . ': ' . $value, replace: false);
×
107
                return $this;
×
108
        }
109

110

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

122

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

133

134
        /**
135
         * Response should be downloaded with 'Save as' dialog.
136
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
137
         */
138
        public function sendAsFile(string $fileName): static
139
        {
140
                $this->setHeader(
×
141
                        'Content-Disposition',
×
142
                        'attachment; filename="' . str_replace('"', '', $fileName) . '"; '
×
143
                        . "filename*=utf-8''" . rawurlencode($fileName),
×
144
                );
145
                return $this;
×
146
        }
147

148

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

163

164
        /**
165
         * Sets the expiration of the HTTP document using the `Cache-Control` and `Expires` headers.
166
         * The parameter is either a time interval (as text) or `null`, which disables caching.
167
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
168
         */
169
        public function setExpiration(?string $expire): static
170
        {
171
                $this->setHeader('Pragma', null);
×
172
                if (!$expire) { // no cache
×
173
                        $this->setHeader('Cache-Control', 's-maxage=0, max-age=0, must-revalidate');
×
174
                        $this->setHeader('Expires', 'Mon, 23 Jan 1978 10:00:00 GMT');
×
175
                        return $this;
×
176
                }
177

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

184

185
        /**
186
         * Returns whether headers have already been sent from the server to the browser,
187
         * so it is no longer possible to send headers or change the response code.
188
         */
189
        public function isSent(): bool
190
        {
191
                return headers_sent();
1✔
192
        }
193

194

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

208
                return null;
×
209
        }
210

211

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

226
                return $headers;
×
227
        }
228

229

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

258

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

273

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

280
                } elseif (
281
                        $this->warnOnBuffer &&
×
282
                        ob_get_length() &&
×
283
                        !array_filter(ob_get_status(full_status: true), fn(array $i): bool => !$i['chunk_size'])
×
284
                ) {
285
                        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.');
×
286
                }
287
        }
1✔
288
}
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