diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index cd65323..00818b7 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -3152,6 +3152,12 @@ function drupal_classloader($class_loader = NULL) { $loader->registerPrefixes($prefixes); $loader->registerNamespaces($namespaces); + // Fallback to symfony SessionHandlerInterface unless php provides one. This + // should be removed once we switched to composer autoload. + if (!interface_exists('SessionHandlerInterface', FALSE)) { + $loader->registerPrefix('SessionHandlerInterface', $namespaces['Symfony\Component\HttpFoundation'] . 'Symfony/Component/HttpFoundation/Resources/stubs'); + } + // Register the loader with PHP. $loader->register(); } diff --git a/core/lib/Drupal/Core/CoreBundle.php b/core/lib/Drupal/Core/CoreBundle.php index e9d6d2f..a20cb4a 100644 --- a/core/lib/Drupal/Core/CoreBundle.php +++ b/core/lib/Drupal/Core/CoreBundle.php @@ -100,6 +100,17 @@ class CoreBundle extends Bundle { ->addMethodCall('addSubscriber', array(new Reference('http_client_simpletest_subscriber'))) ->addMethodCall('setUserAgent', array('Drupal (+http://drupal.org/)')); + // Register the session service. + $container->register('session.storage.backend', 'Drupal\Core\Session\Handler\DatabaseSessionHandler'); + $container->register('session.storage.proxy', 'Drupal\Core\Session\Proxy\CookieOverrideProxy') + ->addArgument(new Reference('session.storage.backend')); + $container->setParameter('session.storage.options', array()); + $container->register('session.storage', 'Drupal\Core\Session\Storage\DrupalSessionStorage') + ->addArgument('%session.storage.options%') + ->addArgument(new Reference('session.storage.proxy')); + $container->register('session', 'Drupal\Core\Session\Session') + ->addArgument(new Reference('session.storage')); + // Register the EntityManager. $container->register('plugin.manager.entity', 'Drupal\Core\Entity\EntityManager'); diff --git a/core/lib/Drupal/Core/Session/Handler/DatabaseSessionHandler.php b/core/lib/Drupal/Core/Session/Handler/DatabaseSessionHandler.php new file mode 100644 index 0000000..5ed5cfa --- /dev/null +++ b/core/lib/Drupal/Core/Session/Handler/DatabaseSessionHandler.php @@ -0,0 +1,87 @@ +condition('sid', $sessionId)->execute(); + } + catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); + } + + return TRUE; + } + + /** + * @Implements SessionHandlerInterface::gc(). + */ + public function gc($lifetime) { + try { + db_delete('sessions')->condition('timestamp', time() - $lifetime, '<')->execute(); + } + catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to manipulate session data: %s', $e->getMessage()), 0, $e); + } + + return TRUE; + } + + /** + * @Implements SessionHandlerInterface::read(). + */ + public function read($sessionId) { + $data = db_query("SELECT s.* FROM {sessions} s WHERE s.sid = :sid", array(':sid' => $sessionId))->fetchObject(); + return !empty($data) ? $data->session : ''; + } + + /** + * @Implements SessionHandlerInterface::write(). + */ + public function write($sessionId, $data) { + try { + db_merge('sessions') + ->key(array( + 'sid' => $sessionId, + )) + ->fields(array( + 'session' => $data, + 'timestamp' => time(), + )) + ->execute(); + } + catch (\PDOException $e) { + throw new \RuntimeException(sprintf('PDOException was thrown when trying to write session data: %s', $e->getMessage()), 0, $e); + } + + return TRUE; + } +} diff --git a/core/lib/Drupal/Core/Session/Proxy/CookieOverrideProxy.php b/core/lib/Drupal/Core/Session/Proxy/CookieOverrideProxy.php new file mode 100644 index 0000000..41062fd --- /dev/null +++ b/core/lib/Drupal/Core/Session/Proxy/CookieOverrideProxy.php @@ -0,0 +1,164 @@ +getIdFromCookie()) { + $this->setId($id); + } + else { + // Set a session identifier for this request. This is necessary because we + // lazily start sessions at the end of this request, and some processes + // (like drupal_get_token()) needs to know the future session ID i + // advance. + $GLOBALS['lazy_session'] = TRUE; + + // Less random sessions (which are much faster to generate) are used for + // anonymous users than are generated in drupal_session_regenerate() when + // a user becomes authenticated. + $this->regenerateId(); + + /* + * @todo Restore HTTPS cookie + if ($is_https && variable_get('https', FALSE)) { + $insecure_session_name = substr(session_name(), 1); + $session_id = drupal_hash_base64(uniqid(mt_rand(), TRUE)); + $_COOKIE[$insecure_session_name] = $session_id; + } + */ + } + } + + /** + * @overrides \Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy::isSessionHandlerInterface(). + */ + public function isSessionHandlerInterface() + { + return true; + } + + /** + * Get current session identifier from cookie, if any. + * + * @return string + * Session identifier or NULL if none found. + */ + protected function getIdFromCookie() { + $name = $this->getName(); + if (!empty($_COOKIE[$name])) { + // @todo Restore HTTPS cookie + //|| ($GLOBALS['is_https'] && variable_get('https', FALSE) && !empty($_COOKIE[substr(session_name(), 1)]))) { + return $_COOKIE[$name]; + } + } + + protected function destroyCookies() { + if (headers_sent()) { + //throw new \RuntimeException('Failed to destroy cookies because headers have already been sent.'); + } + + $params = session_get_cookie_params(); + // @todo Restore HTTPS cookie + setcookie($this->getName(), '', time() - 3600, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + } + + protected function sendCookies($id) { + if (headers_sent()) { + //throw new \RuntimeException('Failed to set cookies because headers have already been sent.'); + } + + $params = session_get_cookie_params(); + $expire = $params['lifetime'] ? time() + $params['lifetime'] : 0; + // @todo Restore HTTPS cookie + setcookie($this->getName(), $id, $expire, $params['path'], $params['domain'], $params['secure'], $params['httponly']); + } + + public function write($id, $data) { + + if (!$this->isActive()) { + return FALSE; + } + + // Cookie sending must be when we are sure we need to keep the session, this + // ensure the lazy session init. Lazy session init is abusive talking we are + // not lazy initializing the session, but lazy sending the session cookie + // instead. Each anonymous user will intrinsecly have a session tied, which + // allows to generate tokens for forms and such, but if the session ends up + // empty, the cookies will not be sent and the session will not be saved on + // disk. + $this->sendCookies($id); + + return (bool) $this->handler->write($id, $data); + } + + public function destroy($id) { + $this->destroyCookies($id); + + return (bool) $this->handler->destroy($id); + } + + /** + * Generate new session identifier. + * + * The the session_regenerate_id() is hardcoded into Symfony's + * NativeSessionStorage implementation while all other session_*() functions + * are used as setters only in the AbstractProxy implementation. This feels + * wrong and we need to override it without doing invasive changes. + * + * @todo Propose a nice PR to Symfony guys so they move this specific call + * into the SessionHandlerProxy so we wouldn't have to override the + * NativeSessionStorage at all. + * + * @see Drupal\Core\Session\Proxy\Storage\DrupalSessionStorage::regenerate() + * + * @param bool $destroy + * (optional) If set to TRUE, destroy the old session. + * + * @return string + * New session identifier. + */ + public function regenerateId($destroy = FALSE) { + $id = drupal_hash_base64(uniqid(mt_rand(), TRUE) . drupal_random_bytes(55)); + + // Do not call parent::setId() here, else it will throw exceptions because + // during session identifier regeneration, this component is considered as + // active. + session_id($id); + + if ($destroy) { + $this->destroyCookies(); + } + + return TRUE; + } +} diff --git a/core/lib/Drupal/Core/Session/Session.php b/core/lib/Drupal/Core/Session/Session.php new file mode 100644 index 0000000..b79d33e --- /dev/null +++ b/core/lib/Drupal/Core/Session/Session.php @@ -0,0 +1,131 @@ +saveEnabled = TRUE; + + if (null !== $this->lastSaveHandlerState && $this->lastSaveHandlerState) { + $this->storage->getSaveHandler()->setActive($this->lastSaveHandlerState); + } + } + + /** + * Disable session save, at commit time session save will be skiped and + * session token will not be sent to client. + * + * This function allows the caller to temporarily disable writing of + * session data, should the request end while performing potentially + * dangerous operations, such as manipulating the global $user object. + * See http://drupal.org/node/218104 for usage. + */ + public function disableSave() { + $this->saveEnabled = FALSE; + + // As a side effect, the save handler in some occasions can be reached by + // either PHP native session handling either Symfony session handling. By + // disabling it manually we ensure it won't save anything behind our back. + $saveHandler = $this->storage->getSaveHandler(); + + if ($this->lastSaveHandlerState = $saveHandler->isActive()) { + $saveHandler->setActive(FALSE); + } + } + + /**sy + * Is the session save enabled. + * + * @return bool + */ + public function isSaveEnabled() { + return $this->saveEnabled; + } + + /** + * Does this session is empty. + * + * @todo This is the most absurd implementation that could ever been written + * but there is no clean solution because bags can not be directly accessed + * via protected attributes, and they don't have either a count() or isEmpty() + * method. + * + * @return bool + * TRUE if session is empty. + */ + public function isEmpty() { + $empty = TRUE; + + // @todo: This code is incredebly ugly and this foreach needs to be removed + // once all Drupal code is ported to use the session bag instead of direct + // $_SESSION superglobal usage. + foreach ($_SESSION as $key => $value) { + // Ignore SF2 attributes + if (0 === strpos($key, '_sf2')) { + continue; + } + $empty = FALSE; + break; + } + + return $empty && !count($this->getFlashBag()->all()) && !count($this->all()); + } + + public function save() { + // Session saving is checked upper, but avoid accidental save() trigger in + // case save is disabled. + // @todo May be should throw a \LogicException here? + if (!$this->isSaveEnabled()) { + return; + } + + parent::save(); + } + + /** + * Overrides Symfony\Component\HttpFoundation\Session\Session::migrate(). + * + * Prevent regenerate if saving is disabled. + */ + public function migrate($destroy = FALSE, $lifetime = NULL) { + + if (!$this->isSaveEnabled()) { + return; + } + + return $this->storage->regenerate($destroy, $lifetime); + } + +} diff --git a/core/lib/Drupal/Core/Session/StaticSessionFactory.php b/core/lib/Drupal/Core/Session/StaticSessionFactory.php new file mode 100644 index 0000000..e015b4b --- /dev/null +++ b/core/lib/Drupal/Core/Session/StaticSessionFactory.php @@ -0,0 +1,45 @@ +setMetadataBag($metaBag); + $this->setOptions($options); + $this->setSaveHandler($handler); + } + + public function clear() { + parent::clear(); + + // Clearing the session is a signal sent when session is invalidated, this + // means we can mark the session handler as inactive so it won't attempt + // any empty session write. Our session handler will send session cookie at + // write time. This allows lazy cookie sending to the client. + $this->saveHandler->setActive(FALSE); + } + + public function regenerate($destroy = FALSE, $lifetime = NULL) { + + if (null !== $lifetime) { + ini_set('session.cookie_lifetime', $lifetime); + } + + if ($destroy) { + $this->metadataBag->stampNew(); + } + + // If the current save handler is our own we must rely its own session + // identifier generation method. I hope Symfony will move this call to + // this object so we can get rid of this method override. + if ($this->saveHandler instanceof CookieOverrideProxy) { + return $this->saveHandler->regenerateId($destroy); + } + else { + return session_regenerate_id($destroy); + } + } +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php b/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php index 02503f7..235ee73 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Session/SessionTest.php @@ -27,6 +27,13 @@ class SessionTest extends WebTestBase { } /** + * Test that a DIC based session can be created. + */ + function testDICSession() { + $session = drupal_container()->get('session'); + } + + /** * Tests for drupal_save_session() and drupal_session_regenerate(). */ function testSessionSaveRegenerate() {