Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 /*
|
Chris@0
|
4 * This file is part of the Symfony package.
|
Chris@0
|
5 *
|
Chris@0
|
6 * (c) Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
7 *
|
Chris@0
|
8 * For the full copyright and license information, please view the LICENSE
|
Chris@0
|
9 * file that was distributed with this source code.
|
Chris@0
|
10 */
|
Chris@0
|
11
|
Chris@0
|
12 namespace Symfony\Component\HttpFoundation;
|
Chris@0
|
13
|
Chris@0
|
14 /**
|
Chris@0
|
15 * ResponseHeaderBag is a container for Response HTTP headers.
|
Chris@0
|
16 *
|
Chris@0
|
17 * @author Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
18 */
|
Chris@0
|
19 class ResponseHeaderBag extends HeaderBag
|
Chris@0
|
20 {
|
Chris@0
|
21 const COOKIES_FLAT = 'flat';
|
Chris@0
|
22 const COOKIES_ARRAY = 'array';
|
Chris@0
|
23
|
Chris@0
|
24 const DISPOSITION_ATTACHMENT = 'attachment';
|
Chris@0
|
25 const DISPOSITION_INLINE = 'inline';
|
Chris@0
|
26
|
Chris@17
|
27 protected $computedCacheControl = [];
|
Chris@17
|
28 protected $cookies = [];
|
Chris@17
|
29 protected $headerNames = [];
|
Chris@0
|
30
|
Chris@17
|
31 public function __construct(array $headers = [])
|
Chris@0
|
32 {
|
Chris@0
|
33 parent::__construct($headers);
|
Chris@0
|
34
|
Chris@0
|
35 if (!isset($this->headers['cache-control'])) {
|
Chris@0
|
36 $this->set('Cache-Control', '');
|
Chris@0
|
37 }
|
Chris@0
|
38
|
Chris@14
|
39 /* RFC2616 - 14.18 says all Responses need to have a Date */
|
Chris@14
|
40 if (!isset($this->headers['date'])) {
|
Chris@14
|
41 $this->initDate();
|
Chris@0
|
42 }
|
Chris@0
|
43 }
|
Chris@0
|
44
|
Chris@0
|
45 /**
|
Chris@0
|
46 * Returns the headers, with original capitalizations.
|
Chris@0
|
47 *
|
Chris@0
|
48 * @return array An array of headers
|
Chris@0
|
49 */
|
Chris@0
|
50 public function allPreserveCase()
|
Chris@0
|
51 {
|
Chris@17
|
52 $headers = [];
|
Chris@14
|
53 foreach ($this->all() as $name => $value) {
|
Chris@14
|
54 $headers[isset($this->headerNames[$name]) ? $this->headerNames[$name] : $name] = $value;
|
Chris@14
|
55 }
|
Chris@14
|
56
|
Chris@14
|
57 return $headers;
|
Chris@14
|
58 }
|
Chris@14
|
59
|
Chris@14
|
60 public function allPreserveCaseWithoutCookies()
|
Chris@14
|
61 {
|
Chris@14
|
62 $headers = $this->allPreserveCase();
|
Chris@14
|
63 if (isset($this->headerNames['set-cookie'])) {
|
Chris@14
|
64 unset($headers[$this->headerNames['set-cookie']]);
|
Chris@14
|
65 }
|
Chris@14
|
66
|
Chris@14
|
67 return $headers;
|
Chris@0
|
68 }
|
Chris@0
|
69
|
Chris@0
|
70 /**
|
Chris@0
|
71 * {@inheritdoc}
|
Chris@0
|
72 */
|
Chris@17
|
73 public function replace(array $headers = [])
|
Chris@0
|
74 {
|
Chris@17
|
75 $this->headerNames = [];
|
Chris@0
|
76
|
Chris@0
|
77 parent::replace($headers);
|
Chris@0
|
78
|
Chris@0
|
79 if (!isset($this->headers['cache-control'])) {
|
Chris@0
|
80 $this->set('Cache-Control', '');
|
Chris@0
|
81 }
|
Chris@14
|
82
|
Chris@14
|
83 if (!isset($this->headers['date'])) {
|
Chris@14
|
84 $this->initDate();
|
Chris@14
|
85 }
|
Chris@14
|
86 }
|
Chris@14
|
87
|
Chris@14
|
88 /**
|
Chris@14
|
89 * {@inheritdoc}
|
Chris@14
|
90 */
|
Chris@14
|
91 public function all()
|
Chris@14
|
92 {
|
Chris@14
|
93 $headers = parent::all();
|
Chris@14
|
94 foreach ($this->getCookies() as $cookie) {
|
Chris@14
|
95 $headers['set-cookie'][] = (string) $cookie;
|
Chris@14
|
96 }
|
Chris@14
|
97
|
Chris@14
|
98 return $headers;
|
Chris@0
|
99 }
|
Chris@0
|
100
|
Chris@0
|
101 /**
|
Chris@0
|
102 * {@inheritdoc}
|
Chris@0
|
103 */
|
Chris@0
|
104 public function set($key, $values, $replace = true)
|
Chris@0
|
105 {
|
Chris@14
|
106 $uniqueKey = str_replace('_', '-', strtolower($key));
|
Chris@14
|
107
|
Chris@14
|
108 if ('set-cookie' === $uniqueKey) {
|
Chris@14
|
109 if ($replace) {
|
Chris@17
|
110 $this->cookies = [];
|
Chris@14
|
111 }
|
Chris@14
|
112 foreach ((array) $values as $cookie) {
|
Chris@14
|
113 $this->setCookie(Cookie::fromString($cookie));
|
Chris@14
|
114 }
|
Chris@14
|
115 $this->headerNames[$uniqueKey] = $key;
|
Chris@14
|
116
|
Chris@14
|
117 return;
|
Chris@14
|
118 }
|
Chris@14
|
119
|
Chris@14
|
120 $this->headerNames[$uniqueKey] = $key;
|
Chris@14
|
121
|
Chris@0
|
122 parent::set($key, $values, $replace);
|
Chris@0
|
123
|
Chris@0
|
124 // ensure the cache-control header has sensible defaults
|
Chris@17
|
125 if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true)) {
|
Chris@0
|
126 $computed = $this->computeCacheControlValue();
|
Chris@17
|
127 $this->headers['cache-control'] = [$computed];
|
Chris@0
|
128 $this->headerNames['cache-control'] = 'Cache-Control';
|
Chris@0
|
129 $this->computedCacheControl = $this->parseCacheControl($computed);
|
Chris@0
|
130 }
|
Chris@0
|
131 }
|
Chris@0
|
132
|
Chris@0
|
133 /**
|
Chris@0
|
134 * {@inheritdoc}
|
Chris@0
|
135 */
|
Chris@0
|
136 public function remove($key)
|
Chris@0
|
137 {
|
Chris@0
|
138 $uniqueKey = str_replace('_', '-', strtolower($key));
|
Chris@0
|
139 unset($this->headerNames[$uniqueKey]);
|
Chris@0
|
140
|
Chris@14
|
141 if ('set-cookie' === $uniqueKey) {
|
Chris@17
|
142 $this->cookies = [];
|
Chris@14
|
143
|
Chris@14
|
144 return;
|
Chris@14
|
145 }
|
Chris@14
|
146
|
Chris@14
|
147 parent::remove($key);
|
Chris@14
|
148
|
Chris@0
|
149 if ('cache-control' === $uniqueKey) {
|
Chris@17
|
150 $this->computedCacheControl = [];
|
Chris@0
|
151 }
|
Chris@14
|
152
|
Chris@14
|
153 if ('date' === $uniqueKey) {
|
Chris@14
|
154 $this->initDate();
|
Chris@14
|
155 }
|
Chris@0
|
156 }
|
Chris@0
|
157
|
Chris@0
|
158 /**
|
Chris@0
|
159 * {@inheritdoc}
|
Chris@0
|
160 */
|
Chris@0
|
161 public function hasCacheControlDirective($key)
|
Chris@0
|
162 {
|
Chris@18
|
163 return \array_key_exists($key, $this->computedCacheControl);
|
Chris@0
|
164 }
|
Chris@0
|
165
|
Chris@0
|
166 /**
|
Chris@0
|
167 * {@inheritdoc}
|
Chris@0
|
168 */
|
Chris@0
|
169 public function getCacheControlDirective($key)
|
Chris@0
|
170 {
|
Chris@18
|
171 return \array_key_exists($key, $this->computedCacheControl) ? $this->computedCacheControl[$key] : null;
|
Chris@0
|
172 }
|
Chris@0
|
173
|
Chris@0
|
174 public function setCookie(Cookie $cookie)
|
Chris@0
|
175 {
|
Chris@0
|
176 $this->cookies[$cookie->getDomain()][$cookie->getPath()][$cookie->getName()] = $cookie;
|
Chris@14
|
177 $this->headerNames['set-cookie'] = 'Set-Cookie';
|
Chris@0
|
178 }
|
Chris@0
|
179
|
Chris@0
|
180 /**
|
Chris@0
|
181 * Removes a cookie from the array, but does not unset it in the browser.
|
Chris@0
|
182 *
|
Chris@0
|
183 * @param string $name
|
Chris@0
|
184 * @param string $path
|
Chris@0
|
185 * @param string $domain
|
Chris@0
|
186 */
|
Chris@0
|
187 public function removeCookie($name, $path = '/', $domain = null)
|
Chris@0
|
188 {
|
Chris@0
|
189 if (null === $path) {
|
Chris@0
|
190 $path = '/';
|
Chris@0
|
191 }
|
Chris@0
|
192
|
Chris@0
|
193 unset($this->cookies[$domain][$path][$name]);
|
Chris@0
|
194
|
Chris@0
|
195 if (empty($this->cookies[$domain][$path])) {
|
Chris@0
|
196 unset($this->cookies[$domain][$path]);
|
Chris@0
|
197
|
Chris@0
|
198 if (empty($this->cookies[$domain])) {
|
Chris@0
|
199 unset($this->cookies[$domain]);
|
Chris@0
|
200 }
|
Chris@0
|
201 }
|
Chris@14
|
202
|
Chris@14
|
203 if (empty($this->cookies)) {
|
Chris@14
|
204 unset($this->headerNames['set-cookie']);
|
Chris@14
|
205 }
|
Chris@0
|
206 }
|
Chris@0
|
207
|
Chris@0
|
208 /**
|
Chris@0
|
209 * Returns an array with all cookies.
|
Chris@0
|
210 *
|
Chris@0
|
211 * @param string $format
|
Chris@0
|
212 *
|
Chris@16
|
213 * @return Cookie[]
|
Chris@0
|
214 *
|
Chris@0
|
215 * @throws \InvalidArgumentException When the $format is invalid
|
Chris@0
|
216 */
|
Chris@0
|
217 public function getCookies($format = self::COOKIES_FLAT)
|
Chris@0
|
218 {
|
Chris@17
|
219 if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) {
|
Chris@17
|
220 throw new \InvalidArgumentException(sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY])));
|
Chris@0
|
221 }
|
Chris@0
|
222
|
Chris@0
|
223 if (self::COOKIES_ARRAY === $format) {
|
Chris@0
|
224 return $this->cookies;
|
Chris@0
|
225 }
|
Chris@0
|
226
|
Chris@17
|
227 $flattenedCookies = [];
|
Chris@0
|
228 foreach ($this->cookies as $path) {
|
Chris@0
|
229 foreach ($path as $cookies) {
|
Chris@0
|
230 foreach ($cookies as $cookie) {
|
Chris@0
|
231 $flattenedCookies[] = $cookie;
|
Chris@0
|
232 }
|
Chris@0
|
233 }
|
Chris@0
|
234 }
|
Chris@0
|
235
|
Chris@0
|
236 return $flattenedCookies;
|
Chris@0
|
237 }
|
Chris@0
|
238
|
Chris@0
|
239 /**
|
Chris@0
|
240 * Clears a cookie in the browser.
|
Chris@0
|
241 *
|
Chris@0
|
242 * @param string $name
|
Chris@0
|
243 * @param string $path
|
Chris@0
|
244 * @param string $domain
|
Chris@0
|
245 * @param bool $secure
|
Chris@0
|
246 * @param bool $httpOnly
|
Chris@0
|
247 */
|
Chris@0
|
248 public function clearCookie($name, $path = '/', $domain = null, $secure = false, $httpOnly = true)
|
Chris@0
|
249 {
|
Chris@0
|
250 $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly));
|
Chris@0
|
251 }
|
Chris@0
|
252
|
Chris@0
|
253 /**
|
Chris@0
|
254 * Generates a HTTP Content-Disposition field-value.
|
Chris@0
|
255 *
|
Chris@0
|
256 * @param string $disposition One of "inline" or "attachment"
|
Chris@0
|
257 * @param string $filename A unicode string
|
Chris@0
|
258 * @param string $filenameFallback A string containing only ASCII characters that
|
Chris@0
|
259 * is semantically equivalent to $filename. If the filename is already ASCII,
|
Chris@0
|
260 * it can be omitted, or just copied from $filename
|
Chris@0
|
261 *
|
Chris@0
|
262 * @return string A string suitable for use as a Content-Disposition field-value
|
Chris@0
|
263 *
|
Chris@0
|
264 * @throws \InvalidArgumentException
|
Chris@0
|
265 *
|
Chris@0
|
266 * @see RFC 6266
|
Chris@0
|
267 */
|
Chris@0
|
268 public function makeDisposition($disposition, $filename, $filenameFallback = '')
|
Chris@0
|
269 {
|
Chris@17
|
270 if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) {
|
Chris@0
|
271 throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE));
|
Chris@0
|
272 }
|
Chris@0
|
273
|
Chris@0
|
274 if ('' == $filenameFallback) {
|
Chris@0
|
275 $filenameFallback = $filename;
|
Chris@0
|
276 }
|
Chris@0
|
277
|
Chris@0
|
278 // filenameFallback is not ASCII.
|
Chris@0
|
279 if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) {
|
Chris@0
|
280 throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.');
|
Chris@0
|
281 }
|
Chris@0
|
282
|
Chris@0
|
283 // percent characters aren't safe in fallback.
|
Chris@0
|
284 if (false !== strpos($filenameFallback, '%')) {
|
Chris@0
|
285 throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.');
|
Chris@0
|
286 }
|
Chris@0
|
287
|
Chris@0
|
288 // path separators aren't allowed in either.
|
Chris@0
|
289 if (false !== strpos($filename, '/') || false !== strpos($filename, '\\') || false !== strpos($filenameFallback, '/') || false !== strpos($filenameFallback, '\\')) {
|
Chris@0
|
290 throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.');
|
Chris@0
|
291 }
|
Chris@0
|
292
|
Chris@0
|
293 $output = sprintf('%s; filename="%s"', $disposition, str_replace('"', '\\"', $filenameFallback));
|
Chris@0
|
294
|
Chris@0
|
295 if ($filename !== $filenameFallback) {
|
Chris@0
|
296 $output .= sprintf("; filename*=utf-8''%s", rawurlencode($filename));
|
Chris@0
|
297 }
|
Chris@0
|
298
|
Chris@0
|
299 return $output;
|
Chris@0
|
300 }
|
Chris@0
|
301
|
Chris@0
|
302 /**
|
Chris@0
|
303 * Returns the calculated value of the cache-control header.
|
Chris@0
|
304 *
|
Chris@0
|
305 * This considers several other headers and calculates or modifies the
|
Chris@0
|
306 * cache-control header to a sensible, conservative value.
|
Chris@0
|
307 *
|
Chris@0
|
308 * @return string
|
Chris@0
|
309 */
|
Chris@0
|
310 protected function computeCacheControlValue()
|
Chris@0
|
311 {
|
Chris@0
|
312 if (!$this->cacheControl && !$this->has('ETag') && !$this->has('Last-Modified') && !$this->has('Expires')) {
|
Chris@0
|
313 return 'no-cache, private';
|
Chris@0
|
314 }
|
Chris@0
|
315
|
Chris@0
|
316 if (!$this->cacheControl) {
|
Chris@0
|
317 // conservative by default
|
Chris@0
|
318 return 'private, must-revalidate';
|
Chris@0
|
319 }
|
Chris@0
|
320
|
Chris@0
|
321 $header = $this->getCacheControlHeader();
|
Chris@0
|
322 if (isset($this->cacheControl['public']) || isset($this->cacheControl['private'])) {
|
Chris@0
|
323 return $header;
|
Chris@0
|
324 }
|
Chris@0
|
325
|
Chris@0
|
326 // public if s-maxage is defined, private otherwise
|
Chris@0
|
327 if (!isset($this->cacheControl['s-maxage'])) {
|
Chris@0
|
328 return $header.', private';
|
Chris@0
|
329 }
|
Chris@0
|
330
|
Chris@0
|
331 return $header;
|
Chris@0
|
332 }
|
Chris@14
|
333
|
Chris@14
|
334 private function initDate()
|
Chris@14
|
335 {
|
Chris@14
|
336 $now = \DateTime::createFromFormat('U', time());
|
Chris@14
|
337 $now->setTimezone(new \DateTimeZone('UTC'));
|
Chris@14
|
338 $this->set('Date', $now->format('D, d M Y H:i:s').' GMT');
|
Chris@14
|
339 }
|
Chris@0
|
340 }
|