| 148 |
lars |
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\Session\Storage;
|
|
|
13 |
|
|
|
14 |
use Symfony\Component\HttpFoundation\Session\SessionBagInterface;
|
|
|
15 |
use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler;
|
|
|
16 |
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy;
|
|
|
17 |
use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy;
|
|
|
18 |
|
|
|
19 |
// Help opcache.preload discover always-needed symbols
|
|
|
20 |
class_exists(MetadataBag::class);
|
|
|
21 |
class_exists(StrictSessionHandler::class);
|
|
|
22 |
class_exists(SessionHandlerProxy::class);
|
|
|
23 |
|
|
|
24 |
/**
|
|
|
25 |
* This provides a base class for session attribute storage.
|
|
|
26 |
*
|
|
|
27 |
* @author Drak <drak@zikula.org>
|
|
|
28 |
*/
|
|
|
29 |
class NativeSessionStorage implements SessionStorageInterface
|
|
|
30 |
{
|
|
|
31 |
/**
|
|
|
32 |
* @var SessionBagInterface[]
|
|
|
33 |
*/
|
|
|
34 |
protected $bags = [];
|
|
|
35 |
|
|
|
36 |
/**
|
|
|
37 |
* @var bool
|
|
|
38 |
*/
|
|
|
39 |
protected $started = false;
|
|
|
40 |
|
|
|
41 |
/**
|
|
|
42 |
* @var bool
|
|
|
43 |
*/
|
|
|
44 |
protected $closed = false;
|
|
|
45 |
|
|
|
46 |
/**
|
|
|
47 |
* @var AbstractProxy|\SessionHandlerInterface
|
|
|
48 |
*/
|
|
|
49 |
protected $saveHandler;
|
|
|
50 |
|
|
|
51 |
/**
|
|
|
52 |
* @var MetadataBag
|
|
|
53 |
*/
|
|
|
54 |
protected $metadataBag;
|
|
|
55 |
|
|
|
56 |
/**
|
|
|
57 |
* Depending on how you want the storage driver to behave you probably
|
|
|
58 |
* want to override this constructor entirely.
|
|
|
59 |
*
|
|
|
60 |
* List of options for $options array with their defaults.
|
|
|
61 |
*
|
|
|
62 |
* @see https://php.net/session.configuration for options
|
|
|
63 |
* but we omit 'session.' from the beginning of the keys for convenience.
|
|
|
64 |
*
|
|
|
65 |
* ("auto_start", is not supported as it tells PHP to start a session before
|
|
|
66 |
* PHP starts to execute user-land code. Setting during runtime has no effect).
|
|
|
67 |
*
|
|
|
68 |
* cache_limiter, "" (use "0" to prevent headers from being sent entirely).
|
|
|
69 |
* cache_expire, "0"
|
|
|
70 |
* cookie_domain, ""
|
|
|
71 |
* cookie_httponly, ""
|
|
|
72 |
* cookie_lifetime, "0"
|
|
|
73 |
* cookie_path, "/"
|
|
|
74 |
* cookie_secure, ""
|
|
|
75 |
* cookie_samesite, null
|
|
|
76 |
* gc_divisor, "100"
|
|
|
77 |
* gc_maxlifetime, "1440"
|
|
|
78 |
* gc_probability, "1"
|
|
|
79 |
* lazy_write, "1"
|
|
|
80 |
* name, "PHPSESSID"
|
|
|
81 |
* referer_check, ""
|
|
|
82 |
* serialize_handler, "php"
|
|
|
83 |
* use_strict_mode, "1"
|
|
|
84 |
* use_cookies, "1"
|
|
|
85 |
* use_only_cookies, "1"
|
|
|
86 |
* use_trans_sid, "0"
|
|
|
87 |
* sid_length, "32"
|
|
|
88 |
* sid_bits_per_character, "5"
|
|
|
89 |
* trans_sid_hosts, $_SERVER['HTTP_HOST']
|
|
|
90 |
* trans_sid_tags, "a=href,area=href,frame=src,form="
|
|
|
91 |
*/
|
|
|
92 |
public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface $handler = null, MetadataBag $metaBag = null)
|
|
|
93 |
{
|
|
|
94 |
if (!\extension_loaded('session')) {
|
|
|
95 |
throw new \LogicException('PHP extension "session" is required.');
|
|
|
96 |
}
|
|
|
97 |
|
|
|
98 |
$options += [
|
|
|
99 |
'cache_limiter' => '',
|
|
|
100 |
'cache_expire' => 0,
|
|
|
101 |
'use_cookies' => 1,
|
|
|
102 |
'lazy_write' => 1,
|
|
|
103 |
'use_strict_mode' => 1,
|
|
|
104 |
];
|
|
|
105 |
|
|
|
106 |
session_register_shutdown();
|
|
|
107 |
|
|
|
108 |
$this->setMetadataBag($metaBag);
|
|
|
109 |
$this->setOptions($options);
|
|
|
110 |
$this->setSaveHandler($handler);
|
|
|
111 |
}
|
|
|
112 |
|
|
|
113 |
/**
|
|
|
114 |
* Gets the save handler instance.
|
|
|
115 |
*/
|
|
|
116 |
public function getSaveHandler(): AbstractProxy|\SessionHandlerInterface
|
|
|
117 |
{
|
|
|
118 |
return $this->saveHandler;
|
|
|
119 |
}
|
|
|
120 |
|
|
|
121 |
public function start(): bool
|
|
|
122 |
{
|
|
|
123 |
if ($this->started) {
|
|
|
124 |
return true;
|
|
|
125 |
}
|
|
|
126 |
|
|
|
127 |
if (\PHP_SESSION_ACTIVE === session_status()) {
|
|
|
128 |
throw new \RuntimeException('Failed to start the session: already started by PHP.');
|
|
|
129 |
}
|
|
|
130 |
|
|
|
131 |
if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL) && headers_sent($file, $line)) {
|
|
|
132 |
throw new \RuntimeException(sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line));
|
|
|
133 |
}
|
|
|
134 |
|
|
|
135 |
$sessionId = $_COOKIE[session_name()] ?? null;
|
|
|
136 |
/*
|
|
|
137 |
* Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`.
|
|
|
138 |
*
|
|
|
139 |
* ---------- Part 1
|
|
|
140 |
*
|
|
|
141 |
* The part `[a-zA-Z0-9,-]` is related to the PHP ini directive `session.sid_bits_per_character` defined as 6.
|
|
|
142 |
* See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-bits-per-character.
|
|
|
143 |
* Allowed values are integers such as:
|
|
|
144 |
* - 4 for range `a-f0-9`
|
|
|
145 |
* - 5 for range `a-v0-9`
|
|
|
146 |
* - 6 for range `a-zA-Z0-9,-`
|
|
|
147 |
*
|
|
|
148 |
* ---------- Part 2
|
|
|
149 |
*
|
|
|
150 |
* The part `{22,250}` is related to the PHP ini directive `session.sid_length`.
|
|
|
151 |
* See https://www.php.net/manual/en/session.configuration.php#ini.session.sid-length.
|
|
|
152 |
* Allowed values are integers between 22 and 256, but we use 250 for the max.
|
|
|
153 |
*
|
|
|
154 |
* Where does the 250 come from?
|
|
|
155 |
* - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255.
|
|
|
156 |
* - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250.
|
|
|
157 |
*
|
|
|
158 |
* ---------- Conclusion
|
|
|
159 |
*
|
|
|
160 |
* The parts 1 and 2 prevent the warning below:
|
|
|
161 |
* `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.`
|
|
|
162 |
*
|
|
|
163 |
* The part 2 prevents the warning below:
|
|
|
164 |
* `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).`
|
|
|
165 |
*/
|
|
|
166 |
if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $sessionId)) {
|
|
|
167 |
// the session ID in the header is invalid, create a new one
|
|
|
168 |
session_id(session_create_id());
|
|
|
169 |
}
|
|
|
170 |
|
|
|
171 |
// ok to try and start the session
|
|
|
172 |
if (!session_start()) {
|
|
|
173 |
throw new \RuntimeException('Failed to start the session.');
|
|
|
174 |
}
|
|
|
175 |
|
|
|
176 |
$this->loadSession();
|
|
|
177 |
|
|
|
178 |
return true;
|
|
|
179 |
}
|
|
|
180 |
|
|
|
181 |
public function getId(): string
|
|
|
182 |
{
|
|
|
183 |
return $this->saveHandler->getId();
|
|
|
184 |
}
|
|
|
185 |
|
|
|
186 |
public function setId(string $id)
|
|
|
187 |
{
|
|
|
188 |
$this->saveHandler->setId($id);
|
|
|
189 |
}
|
|
|
190 |
|
|
|
191 |
public function getName(): string
|
|
|
192 |
{
|
|
|
193 |
return $this->saveHandler->getName();
|
|
|
194 |
}
|
|
|
195 |
|
|
|
196 |
public function setName(string $name)
|
|
|
197 |
{
|
|
|
198 |
$this->saveHandler->setName($name);
|
|
|
199 |
}
|
|
|
200 |
|
|
|
201 |
public function regenerate(bool $destroy = false, int $lifetime = null): bool
|
|
|
202 |
{
|
|
|
203 |
// Cannot regenerate the session ID for non-active sessions.
|
|
|
204 |
if (\PHP_SESSION_ACTIVE !== session_status()) {
|
|
|
205 |
return false;
|
|
|
206 |
}
|
|
|
207 |
|
|
|
208 |
if (headers_sent()) {
|
|
|
209 |
return false;
|
|
|
210 |
}
|
|
|
211 |
|
|
|
212 |
if (null !== $lifetime && $lifetime != \ini_get('session.cookie_lifetime')) {
|
|
|
213 |
$this->save();
|
|
|
214 |
ini_set('session.cookie_lifetime', $lifetime);
|
|
|
215 |
$this->start();
|
|
|
216 |
}
|
|
|
217 |
|
|
|
218 |
if ($destroy) {
|
|
|
219 |
$this->metadataBag->stampNew();
|
|
|
220 |
}
|
|
|
221 |
|
|
|
222 |
return session_regenerate_id($destroy);
|
|
|
223 |
}
|
|
|
224 |
|
|
|
225 |
public function save()
|
|
|
226 |
{
|
|
|
227 |
// Store a copy so we can restore the bags in case the session was not left empty
|
|
|
228 |
$session = $_SESSION;
|
|
|
229 |
|
|
|
230 |
foreach ($this->bags as $bag) {
|
|
|
231 |
if (empty($_SESSION[$key = $bag->getStorageKey()])) {
|
|
|
232 |
unset($_SESSION[$key]);
|
|
|
233 |
}
|
|
|
234 |
}
|
|
|
235 |
if ($_SESSION && [$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) {
|
|
|
236 |
unset($_SESSION[$key]);
|
|
|
237 |
}
|
|
|
238 |
|
|
|
239 |
// Register error handler to add information about the current save handler
|
|
|
240 |
$previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) {
|
|
|
241 |
if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) {
|
|
|
242 |
$handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler;
|
|
|
243 |
$msg = sprintf('session_write_close(): Failed to write session data with "%s" handler', $handler::class);
|
|
|
244 |
}
|
|
|
245 |
|
|
|
246 |
return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false;
|
|
|
247 |
});
|
|
|
248 |
|
|
|
249 |
try {
|
|
|
250 |
session_write_close();
|
|
|
251 |
} finally {
|
|
|
252 |
restore_error_handler();
|
|
|
253 |
|
|
|
254 |
// Restore only if not empty
|
|
|
255 |
if ($_SESSION) {
|
|
|
256 |
$_SESSION = $session;
|
|
|
257 |
}
|
|
|
258 |
}
|
|
|
259 |
|
|
|
260 |
$this->closed = true;
|
|
|
261 |
$this->started = false;
|
|
|
262 |
}
|
|
|
263 |
|
|
|
264 |
public function clear()
|
|
|
265 |
{
|
|
|
266 |
// clear out the bags
|
|
|
267 |
foreach ($this->bags as $bag) {
|
|
|
268 |
$bag->clear();
|
|
|
269 |
}
|
|
|
270 |
|
|
|
271 |
// clear out the session
|
|
|
272 |
$_SESSION = [];
|
|
|
273 |
|
|
|
274 |
// reconnect the bags to the session
|
|
|
275 |
$this->loadSession();
|
|
|
276 |
}
|
|
|
277 |
|
|
|
278 |
public function registerBag(SessionBagInterface $bag)
|
|
|
279 |
{
|
|
|
280 |
if ($this->started) {
|
|
|
281 |
throw new \LogicException('Cannot register a bag when the session is already started.');
|
|
|
282 |
}
|
|
|
283 |
|
|
|
284 |
$this->bags[$bag->getName()] = $bag;
|
|
|
285 |
}
|
|
|
286 |
|
|
|
287 |
public function getBag(string $name): SessionBagInterface
|
|
|
288 |
{
|
|
|
289 |
if (!isset($this->bags[$name])) {
|
|
|
290 |
throw new \InvalidArgumentException(sprintf('The SessionBagInterface "%s" is not registered.', $name));
|
|
|
291 |
}
|
|
|
292 |
|
|
|
293 |
if (!$this->started && $this->saveHandler->isActive()) {
|
|
|
294 |
$this->loadSession();
|
|
|
295 |
} elseif (!$this->started) {
|
|
|
296 |
$this->start();
|
|
|
297 |
}
|
|
|
298 |
|
|
|
299 |
return $this->bags[$name];
|
|
|
300 |
}
|
|
|
301 |
|
|
|
302 |
public function setMetadataBag(MetadataBag $metaBag = null)
|
|
|
303 |
{
|
|
|
304 |
if (1 > \func_num_args()) {
|
|
|
305 |
trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
|
|
|
306 |
}
|
|
|
307 |
$this->metadataBag = $metaBag ?? new MetadataBag();
|
|
|
308 |
|
|
|
309 |
}
|
|
|
310 |
|
|
|
311 |
/**
|
|
|
312 |
* Gets the MetadataBag.
|
|
|
313 |
*/
|
|
|
314 |
public function getMetadataBag(): MetadataBag
|
|
|
315 |
{
|
|
|
316 |
return $this->metadataBag;
|
|
|
317 |
}
|
|
|
318 |
|
|
|
319 |
public function isStarted(): bool
|
|
|
320 |
{
|
|
|
321 |
return $this->started;
|
|
|
322 |
}
|
|
|
323 |
|
|
|
324 |
/**
|
|
|
325 |
* Sets session.* ini variables.
|
|
|
326 |
*
|
|
|
327 |
* For convenience we omit 'session.' from the beginning of the keys.
|
|
|
328 |
* Explicitly ignores other ini keys.
|
|
|
329 |
*
|
|
|
330 |
* @param array $options Session ini directives [key => value]
|
|
|
331 |
*
|
|
|
332 |
* @see https://php.net/session.configuration
|
|
|
333 |
*/
|
|
|
334 |
public function setOptions(array $options)
|
|
|
335 |
{
|
|
|
336 |
if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
|
|
|
337 |
return;
|
|
|
338 |
}
|
|
|
339 |
|
|
|
340 |
$validOptions = array_flip([
|
|
|
341 |
'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly',
|
|
|
342 |
'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite',
|
|
|
343 |
'gc_divisor', 'gc_maxlifetime', 'gc_probability',
|
|
|
344 |
'lazy_write', 'name', 'referer_check',
|
|
|
345 |
'serialize_handler', 'use_strict_mode', 'use_cookies',
|
|
|
346 |
'use_only_cookies', 'use_trans_sid',
|
|
|
347 |
'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags',
|
|
|
348 |
]);
|
|
|
349 |
|
|
|
350 |
foreach ($options as $key => $value) {
|
|
|
351 |
if (isset($validOptions[$key])) {
|
|
|
352 |
if ('cookie_secure' === $key && 'auto' === $value) {
|
|
|
353 |
continue;
|
|
|
354 |
}
|
|
|
355 |
ini_set('session.'.$key, $value);
|
|
|
356 |
}
|
|
|
357 |
}
|
|
|
358 |
}
|
|
|
359 |
|
|
|
360 |
/**
|
|
|
361 |
* Registers session save handler as a PHP session handler.
|
|
|
362 |
*
|
|
|
363 |
* To use internal PHP session save handlers, override this method using ini_set with
|
|
|
364 |
* session.save_handler and session.save_path e.g.
|
|
|
365 |
*
|
|
|
366 |
* ini_set('session.save_handler', 'files');
|
|
|
367 |
* ini_set('session.save_path', '/tmp');
|
|
|
368 |
*
|
|
|
369 |
* or pass in a \SessionHandler instance which configures session.save_handler in the
|
|
|
370 |
* constructor, for a template see NativeFileSessionHandler.
|
|
|
371 |
*
|
|
|
372 |
* @see https://php.net/session-set-save-handler
|
|
|
373 |
* @see https://php.net/sessionhandlerinterface
|
|
|
374 |
* @see https://php.net/sessionhandler
|
|
|
375 |
*
|
|
|
376 |
* @throws \InvalidArgumentException
|
|
|
377 |
*/
|
|
|
378 |
public function setSaveHandler(AbstractProxy|\SessionHandlerInterface $saveHandler = null)
|
|
|
379 |
{
|
|
|
380 |
if (1 > \func_num_args()) {
|
|
|
381 |
trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__);
|
|
|
382 |
}
|
|
|
383 |
|
|
|
384 |
// Wrap $saveHandler in proxy and prevent double wrapping of proxy
|
|
|
385 |
if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) {
|
|
|
386 |
$saveHandler = new SessionHandlerProxy($saveHandler);
|
|
|
387 |
} elseif (!$saveHandler instanceof AbstractProxy) {
|
|
|
388 |
$saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler()));
|
|
|
389 |
}
|
|
|
390 |
$this->saveHandler = $saveHandler;
|
|
|
391 |
|
|
|
392 |
if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
|
|
|
393 |
return;
|
|
|
394 |
}
|
|
|
395 |
|
|
|
396 |
if ($this->saveHandler instanceof SessionHandlerProxy) {
|
|
|
397 |
session_set_save_handler($this->saveHandler, false);
|
|
|
398 |
}
|
|
|
399 |
}
|
|
|
400 |
|
|
|
401 |
/**
|
|
|
402 |
* Load the session with attributes.
|
|
|
403 |
*
|
|
|
404 |
* After starting the session, PHP retrieves the session from whatever handlers
|
|
|
405 |
* are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()).
|
|
|
406 |
* PHP takes the return value from the read() handler, unserializes it
|
|
|
407 |
* and populates $_SESSION with the result automatically.
|
|
|
408 |
*/
|
|
|
409 |
protected function loadSession(array &$session = null)
|
|
|
410 |
{
|
|
|
411 |
if (null === $session) {
|
|
|
412 |
$session = &$_SESSION;
|
|
|
413 |
}
|
|
|
414 |
|
|
|
415 |
$bags = array_merge($this->bags, [$this->metadataBag]);
|
|
|
416 |
|
|
|
417 |
foreach ($bags as $bag) {
|
|
|
418 |
$key = $bag->getStorageKey();
|
|
|
419 |
$session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : [];
|
|
|
420 |
$bag->initialize($session[$key]);
|
|
|
421 |
}
|
|
|
422 |
|
|
|
423 |
$this->started = true;
|
|
|
424 |
$this->closed = false;
|
|
|
425 |
}
|
|
|
426 |
}
|