3 namespace Drupal\Core\Session;
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\DependencyInjection\DependencySerializationTrait;
8 use Symfony\Component\HttpFoundation\RequestStack;
9 use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
12 * Manages user sessions.
14 * This class implements the custom session management code inherited from
15 * Drupal 7 on top of the corresponding Symfony component. Regrettably the name
16 * NativeSessionStorage is not quite accurate. In fact the responsibility for
17 * storing and retrieving session data has been extracted from it in Symfony 2.1
18 * but the class name was not changed.
21 * In fact the NativeSessionStorage class already implements all of the
22 * functionality required by a typical Symfony application. Normally it is not
23 * necessary to subclass it at all. In order to reach the point where Drupal
24 * can use the Symfony session management unmodified, the code implemented
25 * here needs to be extracted either into a dedicated session handler proxy
26 * (e.g. sid-hashing) or relocated to the authentication subsystem.
28 class SessionManager extends NativeSessionStorage implements SessionManagerInterface {
30 use DependencySerializationTrait;
35 * @var \Symfony\Component\HttpFoundation\RequestStack
37 protected $requestStack;
40 * The database connection to use.
42 * @var \Drupal\Core\Database\Connection
44 protected $connection;
47 * The session configuration.
49 * @var \Drupal\Core\Session\SessionConfigurationInterface
51 protected $sessionConfiguration;
54 * Whether a lazy session has been started.
58 protected $startedLazy;
61 * The write safe session handler.
63 * @todo: This reference should be removed once all database queries
64 * are removed from the session manager class.
66 * @var \Drupal\Core\Session\WriteSafeSessionHandlerInterface
68 protected $writeSafeHandler;
71 * Constructs a new session manager instance.
73 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
75 * @param \Drupal\Core\Database\Connection $connection
76 * The database connection.
77 * @param \Drupal\Core\Session\MetadataBag $metadata_bag
78 * The session metadata bag.
79 * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
80 * The session configuration interface.
81 * @param \Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy|Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler|\SessionHandlerInterface|null $handler
82 * The object to register as a PHP session handler.
83 * @see \Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::setSaveHandler()
85 public function __construct(RequestStack $request_stack, Connection $connection, MetadataBag $metadata_bag, SessionConfigurationInterface $session_configuration, $handler = NULL) {
87 $this->sessionConfiguration = $session_configuration;
88 $this->requestStack = $request_stack;
89 $this->connection = $connection;
91 parent::__construct($options, $handler, $metadata_bag);
93 // @todo When not using the Symfony Session object, the list of bags in the
94 // NativeSessionStorage will remain uninitialized. This will lead to
95 // errors in NativeSessionHandler::loadSession. Remove this after
96 // https://www.drupal.org/node/2229145, when we will be using the Symfony
97 // session object (which registers an attribute bag with the
98 // manager upon instantiation).
105 public function start() {
106 if (($this->started || $this->startedLazy) && !$this->closed) {
107 return $this->started;
110 $request = $this->requestStack->getCurrentRequest();
111 $this->setOptions($this->sessionConfiguration->getOptions($request));
113 if ($this->sessionConfiguration->hasSession($request)) {
114 // If a session cookie exists, initialize the session. Otherwise the
115 // session is only started on demand in save(), making
116 // anonymous users not use a session cookie unless something is stored in
117 // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
118 $result = $this->startNow();
121 if (empty($result)) {
122 // Randomly generate a session identifier for this request. This is
123 // necessary because \Drupal\Core\TempStore\SharedTempStoreFactory::get()
124 // wants to know the future session ID of a lazily started session in
127 // @todo: With current versions of PHP there is little reason to generate
128 // the session id from within application code. Consider using the
129 // default php session id instead of generating a custom one:
130 // https://www.drupal.org/node/2238561
131 $this->setId(Crypt::randomBytesBase64());
133 // Initialize the session global and attach the Symfony session bags.
135 $this->loadSession();
137 // NativeSessionStorage::loadSession() sets started to TRUE, reset it to
139 $this->started = FALSE;
140 $this->startedLazy = TRUE;
149 * Forcibly start a PHP session.
152 * TRUE if the session is started.
154 protected function startNow() {
155 if ($this->isCli()) {
159 if ($this->startedLazy) {
160 // Save current session data before starting it, as PHP will destroy it.
161 $session_data = $_SESSION;
164 $result = parent::start();
166 // Restore session data.
167 if ($this->startedLazy) {
168 $_SESSION = $session_data;
169 $this->loadSession();
178 public function save() {
179 if ($this->isCli()) {
180 // We don't have anything to do if we are not allowed to save the session.
184 if ($this->isSessionObsolete()) {
185 // There is no session data to store, destroy the session if it was
186 // previously started.
187 if ($this->getSaveHandler()->isActive()) {
192 // There is session data to store. Start the session if it is not already
194 if (!$this->getSaveHandler()->isActive()) {
197 // Write the session data.
201 $this->startedLazy = FALSE;
207 public function regenerate($destroy = FALSE, $lifetime = NULL) {
208 // Nothing to do if we are not allowed to change the session.
209 if ($this->isCli()) {
213 // We do not support the optional $destroy and $lifetime parameters as long
214 // as #2238561 remains open.
215 if ($destroy || isset($lifetime)) {
216 throw new \InvalidArgumentException('The optional parameters $destroy and $lifetime of SessionManager::regenerate() are not supported currently');
219 if ($this->isStarted()) {
220 $old_session_id = $this->getId();
222 session_id(Crypt::randomBytesBase64());
224 $this->getMetadataBag()->clearCsrfTokenSeed();
226 if (isset($old_session_id)) {
227 $params = session_get_cookie_params();
228 $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
229 setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
230 $this->migrateStoredSession($old_session_id);
233 if (!$this->isStarted()) {
234 // Start the session when it doesn't exist yet.
242 public function delete($uid) {
243 // Nothing to do if we are not allowed to change the session.
244 if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
247 $this->connection->delete('sessions')
248 ->condition('uid', $uid)
255 public function destroy() {
258 // Unset the session cookies.
259 $session_name = $this->getName();
260 $cookies = $this->requestStack->getCurrentRequest()->cookies;
261 // setcookie() can only be called when headers are not yet sent.
262 if ($cookies->has($session_name) && !headers_sent()) {
263 $params = session_get_cookie_params();
264 setcookie($session_name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
265 $cookies->remove($session_name);
272 public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
273 $this->writeSafeHandler = $handler;
277 * Returns whether the current PHP process runs on CLI.
279 * Command line clients do not support cookies nor sessions.
283 protected function isCli() {
284 return PHP_SAPI === 'cli';
288 * Determines whether the session contains user data.
291 * TRUE when the session does not contain any values and therefore can be
294 protected function isSessionObsolete() {
295 $used_session_keys = array_filter($this->getSessionDataMask());
296 return empty($used_session_keys);
300 * Returns a map specifying which session key is containing user data.
303 * An array where keys correspond to the session keys and the values are
304 * booleans specifying whether the corresponding session key contains any
307 protected function getSessionDataMask() {
308 if (empty($_SESSION)) {
312 // Start out with a completely filled mask.
313 $mask = array_fill_keys(array_keys($_SESSION), TRUE);
315 // Ignore the metadata bag, it does not contain any user data.
316 $mask[$this->metadataBag->getStorageKey()] = FALSE;
318 // Ignore attribute bags when they do not contain any data.
319 foreach ($this->bags as $bag) {
320 $key = $bag->getStorageKey();
321 $mask[$key] = !empty($_SESSION[$key]);
324 return array_intersect_key($mask, $_SESSION);
328 * Migrates the current session to a new session id.
330 * @param string $old_session_id
331 * The old session ID. The new session ID is $this->getId().
333 protected function migrateStoredSession($old_session_id) {
334 $fields = ['sid' => Crypt::hashBase64($this->getId())];
335 $this->connection->update('sessions')
337 ->condition('sid', Crypt::hashBase64($old_session_id))