1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace SilverStripe\Control; |
4
|
|
|
|
5
|
|
|
use SilverStripe\ORM\FieldType\DBDatetime; |
6
|
|
|
use LogicException; |
7
|
|
|
use SilverStripe\Core\Extensible; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* A default backend for the setting and getting of cookies |
11
|
|
|
* |
12
|
|
|
* This backend allows one to better test Cookie setting and separate cookie |
13
|
|
|
* handling from the core |
14
|
|
|
* |
15
|
|
|
* @todo Create a config array for defaults (eg: httpOnly, secure, path, domain, expiry) |
16
|
|
|
* @todo A getter for cookies that haven't been sent to the browser yet |
17
|
|
|
* @todo Tests / a way to set the state without hacking with $_COOKIE |
18
|
|
|
* @todo Store the meta information around cookie setting (path, domain, secure, etc) |
19
|
|
|
*/ |
20
|
|
|
class CookieJar implements Cookie_Backend |
21
|
|
|
{ |
22
|
|
|
use Extensible; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* Hold the cookies that were existing at time of instantiation (ie: The ones |
26
|
|
|
* sent to PHP by the browser) |
27
|
|
|
* |
28
|
|
|
* @var array Existing cookies sent by the browser |
29
|
|
|
*/ |
30
|
|
|
protected $existing = []; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Hold the current cookies (ie: a mix of those that were sent to us and we |
34
|
|
|
* have set without the ones we've cleared) |
35
|
|
|
* |
36
|
|
|
* @var array The state of cookies once we've sent the response |
37
|
|
|
*/ |
38
|
|
|
protected $current = []; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Hold any NEW cookies that were set by the application and will be sent |
42
|
|
|
* in the next response |
43
|
|
|
* |
44
|
|
|
* @var array New cookies set by the application |
45
|
|
|
*/ |
46
|
|
|
protected $new = []; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* When creating the backend we want to store the existing cookies in our |
50
|
|
|
* "existing" array. This allows us to distinguish between cookies we received |
51
|
|
|
* or we set ourselves (and didn't get from the browser) |
52
|
|
|
* |
53
|
|
|
* @param array $cookies The existing cookies to load into the cookie jar. |
54
|
|
|
* Omit this to default to $_COOKIE |
55
|
|
|
*/ |
56
|
|
|
public function __construct($cookies = []) |
57
|
|
|
{ |
58
|
|
|
$this->current = $this->existing = func_num_args() |
59
|
|
|
? ($cookies ?: []) // Convert empty values to blank arrays |
60
|
|
|
: $_COOKIE; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Set a cookie |
65
|
|
|
* |
66
|
|
|
* @param string $name The name of the cookie |
67
|
|
|
* @param string $value The value for the cookie to hold |
68
|
|
|
* @param float $expiry The number of days until expiry; 0 indicates a cookie valid for the current session |
69
|
|
|
* @param string $path The path to save the cookie on (falls back to site base) |
70
|
|
|
* @param string $domain The domain to make the cookie available on |
71
|
|
|
* @param boolean $secure Can the cookie only be sent over SSL? |
72
|
|
|
* @param boolean $httpOnly Prevent the cookie being accessible by JS |
73
|
|
|
*/ |
74
|
|
|
public function set($name, $value, $expiry = 90, $path = null, $domain = null, $secure = false, $httpOnly = true) |
75
|
|
|
{ |
76
|
|
|
//are we setting or clearing a cookie? false values are reserved for clearing cookies (see PHP manual) |
77
|
|
|
$clear = false; |
78
|
|
|
if ($value === false || $value === '' || $expiry < 0) { |
79
|
|
|
$clear = true; |
80
|
|
|
$value = false; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
//expiry === 0 is a special case where we set a cookie for the current user session |
84
|
|
|
if ($expiry !== 0) { |
85
|
|
|
//don't do the maths if we are clearing |
86
|
|
|
$expiry = $clear ? -1 : DBDatetime::now()->getTimestamp() + (86400 * $expiry); |
87
|
|
|
} |
88
|
|
|
//set the path up |
89
|
|
|
$path = $path ? $path : Director::baseURL(); |
90
|
|
|
//send the cookie |
91
|
|
|
$this->outputCookie($name, $value, $expiry, $path, $domain, $secure, $httpOnly); |
|
|
|
|
92
|
|
|
//keep our variables in check |
93
|
|
|
if ($clear) { |
94
|
|
|
unset($this->new[$name], $this->current[$name]); |
95
|
|
|
} else { |
96
|
|
|
$this->new[$name] = $this->current[$name] = $value; |
97
|
|
|
} |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Get the cookie value by name |
102
|
|
|
* |
103
|
|
|
* Cookie names are normalised to work around PHP's behaviour of replacing incoming variable name . with _ |
104
|
|
|
* |
105
|
|
|
* @param string $name The name of the cookie to get |
106
|
|
|
* @param boolean $includeUnsent Include cookies we've yet to send when fetching values |
107
|
|
|
* |
108
|
|
|
* @return string|null The cookie value or null if unset |
109
|
|
|
*/ |
110
|
|
|
public function get($name, $includeUnsent = true) |
111
|
|
|
{ |
112
|
|
|
$cookies = $includeUnsent ? $this->current : $this->existing; |
113
|
|
|
if (isset($cookies[$name])) { |
114
|
|
|
return $cookies[$name]; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
//Normalise cookie names by replacing '.' with '_' |
118
|
|
|
$safeName = str_replace('.', '_', $name ?? ''); |
119
|
|
|
if (isset($cookies[$safeName])) { |
120
|
|
|
return $cookies[$safeName]; |
121
|
|
|
} |
122
|
|
|
return null; |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
/** |
126
|
|
|
* Get all the cookies |
127
|
|
|
* |
128
|
|
|
* @param boolean $includeUnsent Include cookies we've yet to send |
129
|
|
|
* @return array All the cookies |
130
|
|
|
*/ |
131
|
|
|
public function getAll($includeUnsent = true) |
132
|
|
|
{ |
133
|
|
|
return $includeUnsent ? $this->current : $this->existing; |
134
|
|
|
} |
135
|
|
|
|
136
|
|
|
/** |
137
|
|
|
* Force the expiry of a cookie by name |
138
|
|
|
* |
139
|
|
|
* @param string $name The name of the cookie to expire |
140
|
|
|
* @param string $path The path to save the cookie on (falls back to site base) |
141
|
|
|
* @param string $domain The domain to make the cookie available on |
142
|
|
|
* @param boolean $secure Can the cookie only be sent over SSL? |
143
|
|
|
* @param boolean $httpOnly Prevent the cookie being accessible by JS |
144
|
|
|
*/ |
145
|
|
|
public function forceExpiry($name, $path = null, $domain = null, $secure = false, $httpOnly = true) |
146
|
|
|
{ |
147
|
|
|
$this->set($name, false, -1, $path, $domain, $secure, $httpOnly); |
|
|
|
|
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* The function that actually sets the cookie using PHP |
152
|
|
|
* |
153
|
|
|
* @see http://uk3.php.net/manual/en/function.setcookie.php |
154
|
|
|
* |
155
|
|
|
* @param string $name The name of the cookie |
156
|
|
|
* @param string|array $value The value for the cookie to hold |
157
|
|
|
* @param int $expiry A Unix timestamp indicating when the cookie expires; 0 means it will expire at the end of the session |
158
|
|
|
* @param string $path The path to save the cookie on (falls back to site base) |
159
|
|
|
* @param string $domain The domain to make the cookie available on |
160
|
|
|
* @param boolean $secure Can the cookie only be sent over SSL? |
161
|
|
|
* @param boolean $httpOnly Prevent the cookie being accessible by JS |
162
|
|
|
* @return boolean If the cookie was set or not; doesn't mean it's accepted by the browser |
163
|
|
|
*/ |
164
|
|
|
protected function outputCookie( |
165
|
|
|
$name, |
166
|
|
|
$value, |
167
|
|
|
$expiry = 90, |
168
|
|
|
$path = null, |
169
|
|
|
$domain = null, |
170
|
|
|
$secure = false, |
171
|
|
|
$httpOnly = true |
172
|
|
|
) { |
173
|
|
|
$sameSite = $this->getSameSite($name); |
|
|
|
|
174
|
|
|
// if headers aren't sent, we can set the cookie |
175
|
|
|
if (!headers_sent($file, $line)) { |
176
|
|
|
return setcookie($name ?? '', $value ?? '', [ |
|
|
|
|
177
|
|
|
'expires' => $expiry ?? 0, |
178
|
|
|
'path' => $path ?? '', |
179
|
|
|
'domain' => $domain ?? '', |
180
|
|
|
'secure' => $sameSite === 'None' ? true : (bool) $secure, |
181
|
|
|
'httponly' => $httpOnly ?? false, |
182
|
|
|
'samesite' => $sameSite, |
183
|
|
|
]); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
if (Cookie::config()->uninherited('report_errors')) { |
187
|
|
|
throw new LogicException( |
188
|
|
|
"Cookie '$name' can't be set. The site started outputting content at line $line in $file" |
189
|
|
|
); |
190
|
|
|
} |
191
|
|
|
return false; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Get the correct samesite value - Session cookies use a different configuration variable. |
196
|
|
|
* |
197
|
|
|
* @deprecated 5.0 The relevant methods will include a `$sameSite` parameter instead. |
198
|
|
|
*/ |
199
|
|
|
private function getSameSite(string $name): string |
200
|
|
|
{ |
201
|
|
|
if ($name === session_name()) { |
202
|
|
|
return Session::config()->get('cookie_samesite'); |
203
|
|
|
} |
204
|
|
|
return Cookie::config()->get('default_samesite'); |
205
|
|
|
} |
206
|
|
|
} |
207
|
|
|
|