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