SessionManager.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <?php
  2. namespace Drupal\Core\Session;
  3. use Drupal\Component\Utility\Crypt;
  4. use Drupal\Core\Database\Connection;
  5. use Drupal\Core\DependencyInjection\DependencySerializationTrait;
  6. use Symfony\Component\HttpFoundation\RequestStack;
  7. use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage;
  8. /**
  9. * Manages user sessions.
  10. *
  11. * This class implements the custom session management code inherited from
  12. * Drupal 7 on top of the corresponding Symfony component. Regrettably the name
  13. * NativeSessionStorage is not quite accurate. In fact the responsibility for
  14. * storing and retrieving session data has been extracted from it in Symfony 2.1
  15. * but the class name was not changed.
  16. *
  17. * @todo
  18. * In fact the NativeSessionStorage class already implements all of the
  19. * functionality required by a typical Symfony application. Normally it is not
  20. * necessary to subclass it at all. In order to reach the point where Drupal
  21. * can use the Symfony session management unmodified, the code implemented
  22. * here needs to be extracted either into a dedicated session handler proxy
  23. * (e.g. sid-hashing) or relocated to the authentication subsystem.
  24. */
  25. class SessionManager extends NativeSessionStorage implements SessionManagerInterface {
  26. use DependencySerializationTrait;
  27. /**
  28. * The request stack.
  29. *
  30. * @var \Symfony\Component\HttpFoundation\RequestStack
  31. */
  32. protected $requestStack;
  33. /**
  34. * The database connection to use.
  35. *
  36. * @var \Drupal\Core\Database\Connection
  37. */
  38. protected $connection;
  39. /**
  40. * The session configuration.
  41. *
  42. * @var \Drupal\Core\Session\SessionConfigurationInterface
  43. */
  44. protected $sessionConfiguration;
  45. /**
  46. * Whether a lazy session has been started.
  47. *
  48. * @var bool
  49. */
  50. protected $startedLazy;
  51. /**
  52. * The write safe session handler.
  53. *
  54. * @todo: This reference should be removed once all database queries
  55. * are removed from the session manager class.
  56. *
  57. * @var \Drupal\Core\Session\WriteSafeSessionHandlerInterface
  58. */
  59. protected $writeSafeHandler;
  60. /**
  61. * Constructs a new session manager instance.
  62. *
  63. * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
  64. * The request stack.
  65. * @param \Drupal\Core\Database\Connection $connection
  66. * The database connection.
  67. * @param \Drupal\Core\Session\MetadataBag $metadata_bag
  68. * The session metadata bag.
  69. * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
  70. * The session configuration interface.
  71. * @param \Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy|Symfony\Component\HttpFoundation\Session\Storage\Handler\NativeSessionHandler|\SessionHandlerInterface|null $handler
  72. * The object to register as a PHP session handler.
  73. * @see \Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage::setSaveHandler()
  74. */
  75. public function __construct(RequestStack $request_stack, Connection $connection, MetadataBag $metadata_bag, SessionConfigurationInterface $session_configuration, $handler = NULL) {
  76. $options = [];
  77. $this->sessionConfiguration = $session_configuration;
  78. $this->requestStack = $request_stack;
  79. $this->connection = $connection;
  80. parent::__construct($options, $handler, $metadata_bag);
  81. // @todo When not using the Symfony Session object, the list of bags in the
  82. // NativeSessionStorage will remain uninitialized. This will lead to
  83. // errors in NativeSessionHandler::loadSession. Remove this after
  84. // https://www.drupal.org/node/2229145, when we will be using the Symfony
  85. // session object (which registers an attribute bag with the
  86. // manager upon instantiation).
  87. $this->bags = [];
  88. }
  89. /**
  90. * {@inheritdoc}
  91. */
  92. public function start() {
  93. if (($this->started || $this->startedLazy) && !$this->closed) {
  94. return $this->started;
  95. }
  96. $request = $this->requestStack->getCurrentRequest();
  97. $this->setOptions($this->sessionConfiguration->getOptions($request));
  98. if ($this->sessionConfiguration->hasSession($request)) {
  99. // If a session cookie exists, initialize the session. Otherwise the
  100. // session is only started on demand in save(), making
  101. // anonymous users not use a session cookie unless something is stored in
  102. // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
  103. $result = $this->startNow();
  104. }
  105. if (empty($result)) {
  106. // Randomly generate a session identifier for this request. This is
  107. // necessary because \Drupal\Core\TempStore\SharedTempStoreFactory::get()
  108. // wants to know the future session ID of a lazily started session in
  109. // advance.
  110. //
  111. // @todo: With current versions of PHP there is little reason to generate
  112. // the session id from within application code. Consider using the
  113. // default php session id instead of generating a custom one:
  114. // https://www.drupal.org/node/2238561
  115. $this->setId(Crypt::randomBytesBase64());
  116. // Initialize the session global and attach the Symfony session bags.
  117. $_SESSION = [];
  118. $this->loadSession();
  119. // NativeSessionStorage::loadSession() sets started to TRUE, reset it to
  120. // FALSE here.
  121. $this->started = FALSE;
  122. $this->startedLazy = TRUE;
  123. $result = FALSE;
  124. }
  125. return $result;
  126. }
  127. /**
  128. * Forcibly start a PHP session.
  129. *
  130. * @return bool
  131. * TRUE if the session is started.
  132. */
  133. protected function startNow() {
  134. if ($this->isCli()) {
  135. return FALSE;
  136. }
  137. if ($this->startedLazy) {
  138. // Save current session data before starting it, as PHP will destroy it.
  139. $session_data = $_SESSION;
  140. }
  141. $result = parent::start();
  142. // Restore session data.
  143. if ($this->startedLazy) {
  144. $_SESSION = $session_data;
  145. $this->loadSession();
  146. }
  147. return $result;
  148. }
  149. /**
  150. * {@inheritdoc}
  151. */
  152. public function save() {
  153. if ($this->isCli()) {
  154. // We don't have anything to do if we are not allowed to save the session.
  155. return;
  156. }
  157. if ($this->isSessionObsolete()) {
  158. // There is no session data to store, destroy the session if it was
  159. // previously started.
  160. if ($this->getSaveHandler()->isActive()) {
  161. $this->destroy();
  162. }
  163. }
  164. else {
  165. // There is session data to store. Start the session if it is not already
  166. // started.
  167. if (!$this->getSaveHandler()->isActive()) {
  168. $this->startNow();
  169. }
  170. // Write the session data.
  171. parent::save();
  172. }
  173. $this->startedLazy = FALSE;
  174. }
  175. /**
  176. * {@inheritdoc}
  177. */
  178. public function regenerate($destroy = FALSE, $lifetime = NULL) {
  179. // Nothing to do if we are not allowed to change the session.
  180. if ($this->isCli()) {
  181. return;
  182. }
  183. // We do not support the optional $destroy and $lifetime parameters as long
  184. // as #2238561 remains open.
  185. if ($destroy || isset($lifetime)) {
  186. throw new \InvalidArgumentException('The optional parameters $destroy and $lifetime of SessionManager::regenerate() are not supported currently');
  187. }
  188. if ($this->isStarted()) {
  189. $old_session_id = $this->getId();
  190. // Save and close the old session. Call the parent method to avoid issue
  191. // with session destruction due to the session being considered obsolete.
  192. parent::save();
  193. // Ensure the session is reloaded correctly.
  194. $this->startedLazy = TRUE;
  195. }
  196. session_id(Crypt::randomBytesBase64());
  197. // We set token seed immediately to avoid race condition between two
  198. // simultaneous requests without a seed.
  199. $this->getMetadataBag()->setCsrfTokenSeed(Crypt::randomBytesBase64());
  200. if (isset($old_session_id)) {
  201. $params = session_get_cookie_params();
  202. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
  203. setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  204. $this->migrateStoredSession($old_session_id);
  205. }
  206. $this->startNow();
  207. }
  208. /**
  209. * {@inheritdoc}
  210. */
  211. public function delete($uid) {
  212. // Nothing to do if we are not allowed to change the session.
  213. if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
  214. return;
  215. }
  216. $this->connection->delete('sessions')
  217. ->condition('uid', $uid)
  218. ->execute();
  219. }
  220. /**
  221. * {@inheritdoc}
  222. */
  223. public function destroy() {
  224. session_destroy();
  225. // Unset the session cookies.
  226. $session_name = $this->getName();
  227. $cookies = $this->requestStack->getCurrentRequest()->cookies;
  228. // setcookie() can only be called when headers are not yet sent.
  229. if ($cookies->has($session_name) && !headers_sent()) {
  230. $params = session_get_cookie_params();
  231. setcookie($session_name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  232. $cookies->remove($session_name);
  233. }
  234. }
  235. /**
  236. * {@inheritdoc}
  237. */
  238. public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
  239. $this->writeSafeHandler = $handler;
  240. }
  241. /**
  242. * Returns whether the current PHP process runs on CLI.
  243. *
  244. * Command line clients do not support cookies nor sessions.
  245. *
  246. * @return bool
  247. */
  248. protected function isCli() {
  249. return PHP_SAPI === 'cli';
  250. }
  251. /**
  252. * Determines whether the session contains user data.
  253. *
  254. * @return bool
  255. * TRUE when the session does not contain any values and therefore can be
  256. * destroyed.
  257. */
  258. protected function isSessionObsolete() {
  259. $used_session_keys = array_filter($this->getSessionDataMask());
  260. return empty($used_session_keys);
  261. }
  262. /**
  263. * Returns a map specifying which session key is containing user data.
  264. *
  265. * @return array
  266. * An array where keys correspond to the session keys and the values are
  267. * booleans specifying whether the corresponding session key contains any
  268. * user data.
  269. */
  270. protected function getSessionDataMask() {
  271. if (empty($_SESSION)) {
  272. return [];
  273. }
  274. // Start out with a completely filled mask.
  275. $mask = array_fill_keys(array_keys($_SESSION), TRUE);
  276. // Ignore the metadata bag, it does not contain any user data.
  277. $mask[$this->metadataBag->getStorageKey()] = FALSE;
  278. // Ignore attribute bags when they do not contain any data.
  279. foreach ($this->bags as $bag) {
  280. $key = $bag->getStorageKey();
  281. $mask[$key] = !empty($_SESSION[$key]);
  282. }
  283. return array_intersect_key($mask, $_SESSION);
  284. }
  285. /**
  286. * Migrates the current session to a new session id.
  287. *
  288. * @param string $old_session_id
  289. * The old session ID. The new session ID is $this->getId().
  290. */
  291. protected function migrateStoredSession($old_session_id) {
  292. $fields = ['sid' => Crypt::hashBase64($this->getId())];
  293. $this->connection->update('sessions')
  294. ->fields($fields)
  295. ->condition('sid', Crypt::hashBase64($old_session_id))
  296. ->execute();
  297. }
  298. }