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

nette / http / 21830508055

09 Feb 2026 03:10PM UTC coverage: 83.772% (+0.03%) from 83.744%
21830508055

push

github

dg
added RequestFactory::setForceHttps()

8 of 9 new or added lines in 2 files covered. (88.89%)

75 existing lines in 9 files now uncovered.

924 of 1103 relevant lines covered (83.77%)

0.84 hits per line

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

47.56
/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, strpos, substr, time;
15
use const ENT_IGNORE, ENT_QUOTES, PHP_SAPI;
16

17

18
/**
19
 * HttpResponse class.
20
 *
21
 * @property-read array $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
                        $a = strpos($header, ':');
×
UNCOV
221
                        $headers[substr($header, 0, $a)] = substr($header, $a + 2);
×
222
                }
223

UNCOV
224
                return $headers;
×
225
        }
226

227

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

256

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

271

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

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