SessionManager.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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. }
  191. session_id(Crypt::randomBytesBase64());
  192. $this->getMetadataBag()->clearCsrfTokenSeed();
  193. if (isset($old_session_id)) {
  194. $params = session_get_cookie_params();
  195. $expire = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
  196. setcookie($this->getName(), $this->getId(), $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  197. $this->migrateStoredSession($old_session_id);
  198. }
  199. if (!$this->isStarted()) {
  200. // Start the session when it doesn't exist yet.
  201. $this->startNow();
  202. }
  203. }
  204. /**
  205. * {@inheritdoc}
  206. */
  207. public function delete($uid) {
  208. // Nothing to do if we are not allowed to change the session.
  209. if (!$this->writeSafeHandler->isSessionWritable() || $this->isCli()) {
  210. return;
  211. }
  212. $this->connection->delete('sessions')
  213. ->condition('uid', $uid)
  214. ->execute();
  215. }
  216. /**
  217. * {@inheritdoc}
  218. */
  219. public function destroy() {
  220. session_destroy();
  221. // Unset the session cookies.
  222. $session_name = $this->getName();
  223. $cookies = $this->requestStack->getCurrentRequest()->cookies;
  224. // setcookie() can only be called when headers are not yet sent.
  225. if ($cookies->has($session_name) && !headers_sent()) {
  226. $params = session_get_cookie_params();
  227. setcookie($session_name, '', REQUEST_TIME - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
  228. $cookies->remove($session_name);
  229. }
  230. }
  231. /**
  232. * {@inheritdoc}
  233. */
  234. public function setWriteSafeHandler(WriteSafeSessionHandlerInterface $handler) {
  235. $this->writeSafeHandler = $handler;
  236. }
  237. /**
  238. * Returns whether the current PHP process runs on CLI.
  239. *
  240. * Command line clients do not support cookies nor sessions.
  241. *
  242. * @return bool
  243. */
  244. protected function isCli() {
  245. return PHP_SAPI === 'cli';
  246. }
  247. /**
  248. * Determines whether the session contains user data.
  249. *
  250. * @return bool
  251. * TRUE when the session does not contain any values and therefore can be
  252. * destroyed.
  253. */
  254. protected function isSessionObsolete() {
  255. $used_session_keys = array_filter($this->getSessionDataMask());
  256. return empty($used_session_keys);
  257. }
  258. /**
  259. * Returns a map specifying which session key is containing user data.
  260. *
  261. * @return array
  262. * An array where keys correspond to the session keys and the values are
  263. * booleans specifying whether the corresponding session key contains any
  264. * user data.
  265. */
  266. protected function getSessionDataMask() {
  267. if (empty($_SESSION)) {
  268. return [];
  269. }
  270. // Start out with a completely filled mask.
  271. $mask = array_fill_keys(array_keys($_SESSION), TRUE);
  272. // Ignore the metadata bag, it does not contain any user data.
  273. $mask[$this->metadataBag->getStorageKey()] = FALSE;
  274. // Ignore attribute bags when they do not contain any data.
  275. foreach ($this->bags as $bag) {
  276. $key = $bag->getStorageKey();
  277. $mask[$key] = !empty($_SESSION[$key]);
  278. }
  279. return array_intersect_key($mask, $_SESSION);
  280. }
  281. /**
  282. * Migrates the current session to a new session id.
  283. *
  284. * @param string $old_session_id
  285. * The old session ID. The new session ID is $this->getId().
  286. */
  287. protected function migrateStoredSession($old_session_id) {
  288. $fields = ['sid' => Crypt::hashBase64($this->getId())];
  289. $this->connection->update('sessions')
  290. ->fields($fields)
  291. ->condition('sid', Crypt::hashBase64($old_session_id))
  292. ->execute();
  293. }
  294. }