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

nette / http / 26789762594

02 Jun 2026 12:03AM UTC coverage: 83.886% (-0.03%) from 83.912%
26789762594

push

github

dg
Helpers: added expirationToSeconds() unifying expiration parsing

A numeric value (including a numeric string) is taken directly as the number of
seconds, a DateTimeInterface or a textual string (e.g. '20 minutes',
'2024-01-01') is resolved as an absolute time, and null means "no value". An
empty string is rejected as it is never meaningful.

The helper is a pure parser and applies no policy - each caller decides what
null or a non-positive result means in its own context:

- Response::setExpiration(): null or a non-positive time disables caching
- Session::setExpiration(): null restores the default lifetime, a non-positive
  time throws (a lifetime in the past makes no sense)
- SessionSection::setExpiration(): null clears the expiration
- Response::setCookie(): null is a session cookie, a non-positive time deletes
  it; passing integer 0 (which used to mean a session cookie) is deprecated in
  favour of null

23 of 31 new or added lines in 4 files covered. (74.19%)

34 existing lines in 3 files now uncovered.

1062 of 1266 relevant lines covered (83.89%)

0.84 hits per line

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

47.13
/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, setcookie, 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✔
UNCOV
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✔
UNCOV
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
        {
UNCOV
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✔
UNCOV
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);
×
UNCOV
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);
×
UNCOV
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 : ''));
×
UNCOV
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) . '"; '
×
UNCOV
140
                        . "filename*=utf-8''" . rawurlencode($fileName),
×
141
                );
UNCOV
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
        {
NEW
168
                $seconds = Helpers::expirationToSeconds($expire);
×
169
                $this->setHeader('Pragma', null);
×
NEW
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

NEW
176
                $this->setHeader('Cache-Control', 'max-age=' . $seconds);
×
NEW
177
                $this->setHeader('Expires', Helpers::formatDate(time() + $seconds));
×
UNCOV
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) {
×
UNCOV
200
                                return ltrim(substr($item, $len));
×
201
                        }
202
                }
203

UNCOV
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])) {
×
UNCOV
218
                                $headers[$parts[0]] = ltrim($parts[1]);
×
219
                        }
220
                }
221

UNCOV
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
        ): static
241
        {
242
                self::checkHeaders();
1✔
243
                if ($expire === 0) { // BC: 0 used to mean a session cookie
1✔
NEW
244
                        trigger_error('Passing 0 as $expire is deprecated; use null for a session cookie.', E_USER_DEPRECATED);
×
NEW
245
                        $expire = null;
×
246
                }
247

248
                $seconds = Helpers::expirationToSeconds($expire);
1✔
249
                setcookie($name, $value, [
1✔
250
                        'expires' => $seconds === null ? 0 : time() + $seconds,
1✔
251
                        'path' => $path ?? ($domain ? '/' : $this->cookiePath),
1✔
252
                        'domain' => $domain ?? ($path ? '' : $this->cookieDomain),
1✔
253
                        'secure' => $secure ?? $this->cookieSecure,
1✔
254
                        'httponly' => $httpOnly ?? true,
1✔
255
                        'samesite' => $sameSite ?? self::SameSiteLax,
1✔
256
                ]);
257
                return $this;
1✔
258
        }
259

260

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

275

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

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