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

nette / http / 26792206728

02 Jun 2026 01:12AM UTC coverage: 83.841% (+0.04%) from 83.803%
26792206728

push

github

dg
Response: setCookie() supports the Partitioned (CHIPS) attribute

Adds a $partitioned argument to setCookie(). When enabled it appends the
Partitioned attribute and forces Secure, which the browser requires for a
partitioned cookie. Like $sameSite, the argument lives only on Response, not on
the IResponse interface.

3 of 4 new or added lines in 1 file covered. (75.0%)

6 existing lines in 1 file now uncovered.

1074 of 1281 relevant lines covered (83.84%)

0.84 hits per line

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

51.0
/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 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, str_replace, strcasecmp, strlen, strncasecmp, substr, time;
12
use const PHP_SAPI;
13

14

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

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

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

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

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

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

39

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

47

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

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

67

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

76

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

92
                return $this;
1✔
93
        }
94

95

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

107

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

119

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

130

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

145

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

160

161
        /**
162
         * Sets the Cache-Control and Expires headers. Pass a time string (e.g. '20 minutes') to enable caching,
163
         * or null to disable it entirely.
164
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
165
         */
166
        public function setExpiration(?string $expire): static
167
        {
168
                $seconds = Helpers::expirationToSeconds($expire);
×
169
                $this->setHeader('Pragma', null);
×
170
                if ($seconds === null || $seconds <= 0) { // 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
                $this->setHeader('Cache-Control', 'max-age=' . $seconds);
×
177
                $this->setHeader('Expires', Helpers::formatDate(time() + $seconds));
×
178
                return $this;
×
179
        }
180

181

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

190

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

204
                return null;
×
205
        }
206

207

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

222
                return $headers;
×
223
        }
224

225

226
        /**
227
         * Sends a cookie.
228
         * @param self::SameSite*|null  $sameSite
229
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
230
         */
231
        public function setCookie(
1✔
232
                string $name,
233
                string $value,
234
                string|int|\DateTimeInterface|null $expire,
235
                ?string $path = null,
236
                ?string $domain = null,
237
                ?bool $secure = null,
238
                ?bool $httpOnly = null,
239
                ?string $sameSite = null,
240
                bool $partitioned = false,
241
        ): static
242
        {
243
                self::checkHeaders();
1✔
244
                [$path, $domain] = [ // resolve the defaults first, so the final values (incl. those from cookiePath/cookieDomain) are validated
1✔
245
                        $path ?? ($domain ? '/' : $this->cookiePath),
1✔
246
                        $domain ?? ($path ? '' : $this->cookieDomain),
1✔
247
                ];
248
                // name, path, domain and sameSite are written into the header verbatim (the value is encoded), so they must be
249
                // validated to prevent attribute injection (setcookie() does the same); the name additionally cannot be encoded,
250
                // because PHP does not url-decode cookie names when reading $_COOKIE
251
                if ($name === '' || preg_match('#[=,; \t\r\n\x0B\x0C]#', $name)) {
1✔
UNCOV
252
                        throw new Nette\InvalidArgumentException("Cookie name must not be empty or contain '=', ',', ';', whitespace or control characters, '$name' given.");
×
253
                } elseif (preg_match('#[,; \t\r\n\x0B\x0C]#', $path . $domain . $sameSite)) {
1✔
254
                        throw new Nette\InvalidArgumentException("Cookie path, domain and sameSite must not contain ',', ';', whitespace or control characters.");
×
255
                } elseif ($expire === 0) { // BC: 0 used to mean a session cookie
1✔
UNCOV
256
                        trigger_error('Passing 0 as $expire is deprecated; use null for a session cookie.', E_USER_DEPRECATED);
×
UNCOV
257
                        $expire = null;
×
258
                }
259

260
                $seconds = Helpers::expirationToSeconds($expire);
1✔
261
                $sameSite ??= self::SameSiteLax;
1✔
262
                // both SameSite=None and Partitioned are rejected by the browser without the Secure attribute
263
                $secure = $sameSite === self::SameSiteNone || $partitioned
1✔
NEW
264
                        ? true
×
265
                        : $secure ?? $this->cookieSecure;
1✔
266
                // the value is raw-url-encoded the same way PHP reads it back from $_COOKIE;
267
                // Max-Age takes precedence over expires (RFC 6265), expires is sent too for ancient clients
268
                $cookie = $name . '=' . rawurlencode($value)
1✔
269
                        . ($seconds === null ? '' : '; expires=' . Helpers::formatDate(time() + $seconds) . '; Max-Age=' . max(0, $seconds))
1✔
270
                        . '; path=' . $path
1✔
271
                        . ($domain === '' ? '' : '; domain=' . $domain)
1✔
272
                        . ($secure ? '; secure' : '')
1✔
273
                        . (($httpOnly ?? true) ? '; HttpOnly' : '')
1✔
274
                        . '; SameSite=' . $sameSite
1✔
275
                        . ($partitioned ? '; Partitioned' : '');
1✔
276
                header('Set-Cookie: ' . $cookie, replace: false);
1✔
277
                return $this;
1✔
278
        }
279

280

281
        /**
282
         * Deletes a cookie.
283
         * @throws Nette\InvalidStateException  if HTTP headers have been sent
284
         */
285
        public function deleteCookie(
1✔
286
                string $name,
287
                ?string $path = null,
288
                ?string $domain = null,
289
                ?bool $secure = null,
290
        ): void
291
        {
292
                $this->setCookie($name, '', -1, $path, $domain, $secure); // a past time => Max-Age=0 => delete now
1✔
293
        }
1✔
294

295

296
        private function checkHeaders(): void
297
        {
298
                if (PHP_SAPI === 'cli') {
1✔
299
                } elseif (headers_sent($file, $line)) {
×
UNCOV
300
                        throw new Nette\InvalidStateException('Cannot send header after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.'));
×
301

302
                } elseif (
303
                        $this->warnOnBuffer &&
×
304
                        ob_get_length() &&
×
UNCOV
305
                        !array_filter(ob_get_status(full_status: true), fn(array $i): bool => !$i['chunk_size'])
×
306
                ) {
UNCOV
307
                        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.');
×
308
                }
309
        }
1✔
310
}
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