Problem/Motivation

In order to move towards #1858196: [meta] Leverage Symfony Session components it is necessary that SessionHandler introduced in #2228341: Objectify session management functions + remove session.inc implements the Symfony SessionStorageInterface. A service implementing SessionStorageInterface is a requirement for the introduction of the Symfony Session. In Symfony applications an instance of this class is accessible through Request::getSession(). This is the preferred way of accessing session data, especially because this will lazy load the session on access.

There are two possible options to reach that goal:

  1. Make SessionHandler a subclass of NativeSessionStorage.
  2. Implement all methods declared by SessionStorageInterface directly in SessionHandler.

Please note that despite the quite confusing name NativeSessionStorage in fact corresponds directly to Drupal SessionManager. The session system changed a lot since Symfony 2.0. The storage-part of the NativeSessionStorage class was subsequently extracted into NativeSessionHandler. At the same time it shifted from the simple storage-class to the current manager-class but unfortunately the name was not changed.

Proposed resolution

The NativeSessionStorage class already implements all of SessionStorageInterface. Thus let's reuse as much as possible from NativeSessionStorage by rebasing SessionHandler onto NativeSessionStorage and gradually remove redundant code from SessionHandler.

Remaining tasks

  • review

User interface changes

API changes

Related Issues

Follow-ups

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

znerol’s picture

Status: Active » Needs review
FileSize
9.18 KB
cosmicdreams’s picture

OMG What? It's that simple? Well I'm happy it passes.

However, I'm not sure we have enough tests. Can we Unit Test the SessionManager?

  1. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -49,26 +51,37 @@ class SessionManager implements SessionManagerInterface {
    +  protected static $anyEnabled = TRUE;
    ...
    +  protected static $anyStarted = FALSE;
    

    Why are we renaming these variables?

  2. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -167,7 +180,7 @@ public function save() {
    +      parent::save();
    

    Awesome

  3. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -297,4 +316,45 @@ protected function isCli() {
    +    // If anything is remaining, this is data set directly on the $_SESSION
    +    // superglobal.
    +    return empty($remaining_keys);
    

    Freaking Awesome

znerol’s picture

However, I'm not sure we have enough tests. Can we Unit Test the SessionManager?

The simpletest-coverage inherited from D7 is pretty comprehensive. In fact I spent a considerable amount of time in the debugger examining test failures before I did understand the new interactions with e.g. MetadataBag::$updateThreshold.

Neither SessionManager nor SessionHandler are unit-testable (i.e. PHPUnit) at the moment because they call the built-in PHP session and cookie functions directly.

Why are we renaming these variables?

Because NativeSessionStorage already has a private non-static attribute with the name $started.

znerol’s picture

Replaced all calls to session_name() with $this->getName() and $this->setName(). Replaced all but one call to session_id() with $this->getId() and $this->setId(). The problem with the remaining invocation in SessionManager::regenerate() is that [...]\HttpFoundation\Session\Storage\Proxy\AbstractProxy::setId() throws an exception when it is called after a session has been started. Symfony uses the standard PHP session id, so let's figure out whether it is acceptable for Drupal to throw overboard the custom session-id code: #2238561: Use the default PHP session ID instead of generating a custom one.

Also removed the comment about "less random sessions" which does not apply anymore after Drupal 7.24.

znerol’s picture

Not so sure whether this is still in scope: Use ..\HttpFoundation\Session\Storage\Handler\WriteCheckSessionHandler instead of the custom access check to implement the session_write_interval setting as well as the write-barrier when no session data changes.

Also extend [...]\HttpFoundation\Session\Storage\MetadataBag such that it can be configured via settings.php. I plan to add more methods to this class in order to be able to transport the Drupal-specific uid and hostname properties between SessionHandler and the cookie authentication provider. This will help with extracting everything related to $user from SessionManager and SessionHandler.

Status: Needs review » Needs work

The last submitted patch, 5: 2238087-rebase-session-manager-5.diff, failed testing.

sun’s picture

  1. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -267,14 +285,14 @@ public function delete($uid) {
       public function isEnabled() {
    -    return static::$enabled;
    +    return static::$anyEnabled;
    

    Perhaps we should rename the internal/protected static property name to $locked now?

    We'd just negate its value then (and keep the existing methods).

    Just an idea. Not sure.

    ...The concept is a bit flawed anyway... When session saving is disabled and re-enabled, then we are still saving potentially unsecure data, because the original session (from the time at which session saving was disabled) has not been cloned/retained as a separate copy (which could then be restored).

  2. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -297,4 +315,45 @@ protected function isCli() {
    +    $remaining_keys = array_flip(array_keys($session));
    ...
    +      unset($remaining_keys[$key]);
    ...
    +    unset($remaining_keys[$this->metadataBag->getStorageKey()]);
    ...
    +    return empty($remaining_keys);
    

    Somehow I've the impression this code could be vastly simplified by using array_diff_key() ?

znerol’s picture

Status: Needs work » Needs review
FileSize
17.83 KB
5.61 KB

Reupload of 5, this time including the \Drupal\Core\Session\MetadataBag. Interdiff is from #4.

Status: Needs review » Needs work

The last submitted patch, 8: 2238087-rebase-session-manager-8.diff, failed testing.

pwolanin’s picture

Can you explain in the summery in more detail the motivation for using the symfony code here?

znerol’s picture

znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
znerol’s picture

Issue summary: View changes
sun’s picture

#7 wasn't addressed yet.

Additionally reviewed the latest patch:

  1. +++ b/core/core.services.yml
    @@ -723,9 +723,12 @@ services:
    +  session_manager.metadata_bag:
    ...
       session_manager:
    

    1) Can we move this definition below session_manager? (as it's more specific)

    2) Could we rename the service ID to just session_metadata? → That would inherently make the Settings name consistent withi that.

  2. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -0,0 +1,30 @@
    +    $storage_key = $settings->get('session_metadata_key', '_sf2_meta');
    

    Is there any sensible use-case for overriding this key?

    If so, it would be good to add an inline comment that states so accordingly; i.e.:

    // Allow ... to ... in case ...

    If not, then we could consider to remove this setting?

  3. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -126,8 +138,8 @@ public function start() {
    +    parent::start();
    +    static::$anyStarted = TRUE;
    

    Is there actually a difference between the two, aside from ours being static?

    If there is no difference, then I think it would be time to figure out how we can get rid of this static. — Perhaps we should attempt that in a separate sister issue first?

    Created #2239181: SessionManager::isStarted() returns TRUE even after session_write_close() has been called

znerol’s picture

Status: Needs work » Needs review
FileSize
16.93 KB
4.16 KB

Addressed #7 and #15.

I still need to hammer a bit on SessionManager::isEmptySession. Perhaps it gets better when I remove the early returns.

Maybe the static $started property better translates to AbstractProxy::isActive(), let's see whether #2239181-5: SessionManager::isStarted() returns TRUE even after session_write_close() has been called returns green.

thelmer’s picture

When the SessionManager::regenerate() is called by eg. user_login_finalize($this->user) the session id invalid:

Warning: session_start(): The session id is too long or contains illegal characters, valid characters are a-z, A-Z, 0-9 and '-,' in Drupal\Core\Session\SessionManager->start() (line 129 of /sites/drupal8/core/lib/Drupal/Core/Session/SessionManager.php).

( fresh git checkout )

znerol’s picture

@thelmer: This patch is not yet committed, therefore the problem you are experiencing cannot possibly be related the work being done here. Please open a new bug-report.

znerol’s picture

Removed most static calls to Settings::get(). We need to find another place for the one which remains in SessionHandler::write() anyway, a session save handler is not supposed to write into the users table in the first place. However untangling the user and the session code is out of scope here.

znerol’s picture

Then finally reindent the chunk of code in SessionHandler::write() where we removed the write-check condition (see #5).

I consider this is ready for the moment. Together with #2239181: SessionManager::isStarted() returns TRUE even after session_write_close() has been called this will bring us one step closer to #1858196: [meta] Leverage Symfony Session components.

znerol’s picture

FileSize
3.7 KB

Interdiff for #20.

znerol’s picture

Reroll after #2239181: SessionManager::isStarted() returns TRUE even after session_write_close() has been called. Also remove SessionManager::isStarted() (delegate to base class).

YesCT’s picture

adding #2245003: Use a random seed instead of the session_id for CSRF token generation to the issue summary, because this issue is not "blocking" that one (yet) but getting this in would help there.

cosmicdreams’s picture

So it seems as if this is ready to go. It just needs some serious reviews. I tried to review this with dreditor but I think I need to apply the patch and review this in context of the rest of the code.

YesCT’s picture

mostly just nits. still needs a good read through.

  1. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -0,0 +1,29 @@
    + * Contains \Drupal\Core\Session\MetadataBag
    

    missing period at end of sentence.

  2. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -0,0 +1,29 @@
    + * Metadata container for application specific session metadata.
    

    https://drupal.org/coding-standards/docs#classes

    "Use a third person verb to start the summary of a class, interface, or method. For example: "Represents a ..." or "Provides..."."

    Tried to figure out what a 'Bag' does...
    "A bag contains variables or parameters. http://stackoverflow.com/questions/19798108/in-symfony-what-is-a-bag"

  3. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -0,0 +1,29 @@
    +   * Construct a new metadata bag instance.
    

    similar. Constructs

  4. +++ b/core/lib/Drupal/Core/Session/MetadataBag.php
    @@ -0,0 +1,29 @@
    +   * @param \Drupal\Core\Utility\Settings $settings
    

    there is no such class. :)
    Core -> Component

  5. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -54,14 +64,29 @@ class SessionManager implements SessionManagerInterface {
    +    // @todo: When not using the Symfony Session object, the list of bags in the
    +    // NativeSessionStorage will remain uninitialized. This will lead to errors
    +    // in NativeSessionHandler::loadSession. Remove this as soon as we are using
    +    // the Symfony session object (which registers an attribute bag with the
    +    // manager upon instantiation).
    +    $this->bags = array();
    

    reformatting (https://drupal.org/coding-standards/docs#todo),

    and adding the link to the actual issue #2229145: Register symfony session components in the DIC and inject the session service into the request object

    But maybe this todo actually needs it's own issue which would be a follow-up of 2229145.

  6. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -289,4 +334,39 @@ protected function isCli() {
    +   * Test whether the session is empty.
    

    Third person verb again.

  7. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -289,4 +334,39 @@ protected function isCli() {
    +   *
    +   * @return bool
    +   *   TRUE when the session does not contain any values.
    +   */
    +  protected function isEmptySession(array $session = NULL) {
    

    adding @param

    Is it an array of sessions, or.. an array of info that describes a session?

    Looked at the usages, and both calls do not pass in an argument. So I'm not sure what $session might be.

  8. +++ b/core/lib/Drupal/Core/Session/SessionManagerInterface.php
    @@ -77,4 +57,24 @@ public function disable();
    +   * Whether or not to enable mixed mode SSL sessions in the session manager.
    +   *
    +   * @param bool $mixed_mode
    +   *   New value for the mixed mode SSL sessions flag.
    +   */
    +  public function setMixedMode($mixed_mode);
    

    changing the doc to Enables or disables.

    But, question: should this also return $this; ?
    I think that's a common pattern for set methods so that we can do chaining.

  9. +++ b/core/lib/Drupal/Core/Session/SessionManagerInterface.php
    @@ -77,4 +57,24 @@ public function disable();
    +   * The name of the insecure session when operating in mixed mode SSL.
    +   */
    +  public function getInsecureName();
     }
    

    adding a @return
    https://drupal.org/coding-standards/docs#return

    (and the newline before the class end } )
    "Leave an empty line between end of method and end of class definition:"
    https://drupal.org/node/608152#indenting

YesCT’s picture

  1. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -72,18 +98,20 @@ public function initialize() {
    -    $handler = new SessionHandler($this, $this->requestStack, $this->connection);
    -    session_set_save_handler($handler, TRUE);
    +    $save_handler = new SessionHandler($this, $this->requestStack, $this->connection);
    +    $write_check_handler = new WriteCheckSessionHandler($save_handler);
    +    $this->setSaveHandler($write_check_handler);
    

    this made my head spin. I think I read it 5 time.

    we had a save handler, then we got a write_check handler, then we set the save handler to the write_check handler. maybe this makes sense to people who know about sessions?

  2. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -72,18 +98,20 @@ public function initialize() {
    -    if (($cookies->has(session_name()) && ($session_name = $cookies->get(session_name()))) || ($is_https && Settings::get('mixed_mode_sessions', FALSE) && ($cookies->has(substr(session_name(), 1))) && ($session_name = $cookies->get(substr(session_name(), 1))))) {
    +    $insecure_session_name = $this->getInsecureName();
    +    if (($cookies->has($this->getName()) && ($session_name = $cookies->get($this->getName()))) || ($is_https && $this->isMixedMode() && ($cookies->has($insecure_session_name) && ($session_name = $cookies->get($insecure_session_name))))) {
    
    @@ -94,12 +122,8 @@ public function initialize() {
    -        $insecure_session_name = substr(session_name(), 1);
    

    ah, I see, the setting the insecure name was moved up before the if, out of the else part, since it was used in the condition up there anyway.

  3. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -94,12 +122,8 @@ public function initialize() {
    -      // Less random sessions (which are much faster to generate) are used for
    -      // anonymous users than are generated in regenerate() when
    -      // a user becomes authenticated.
    -      session_id(Crypt::randomBytesBase64());
    -      if ($is_https && Settings::get('mixed_mode_sessions', FALSE)) {
    

    why did we delete this comment. was it incorrect?

    it might have been explaining why we use 64 instead of something else? i'm not sure what it is less random than.

znerol’s picture

Thanks for the cleanup in #25, regarding #26:

  1. Symfony encourages nesting session save handlers. The innermost deals directly with the storage backend, while any number of wrappers implement application specific logic. The write-check session save handler is in fact the Symfony counterpart of the Drupal mechanism which prohibits frequent session writes when session data is not changing. It is very likely that additional wrappers will be extracted from the current session save handler (e.g. the save handler dealing with storage in order to remove complexity and improve testability of the compontents (database, mongo, etc.), the mixed-mode ssl stuff or the session-id-hash code). Some of them need to be exposed as a service. However, at this time I'm not sure which ones (besides the innermost, a.k.a. the thing which deals with the storage). I prefer to postpone this decision, and only define the services when the whole picture is clearer.
  2. Exactly (I hate this monster-condition).
  3. Up until Drupal 7.24, session-ids for authenticated users were calculated differently from those of anonymous users (see also #2140433: Port SA-CORE-2013-003 to Drupal 8 and the specific change to session.inc. I hope that we can get rid of the Drupal-specific session-id generation at some time: #2238561: Use the default PHP session ID instead of generating a custom one (nice to have, though. No strict requirement).
YesCT’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

Oh, I think the issue summary was updated after #10 asked for it. removing that tag.

Updating remaining tasks.

znerol’s picture

Status: Needs review » Needs work

The last submitted patch, 29: 2238087-rebase-session-manager-29.diff, failed testing.

znerol’s picture

Status: Needs work » Needs review
FileSize
21.31 KB
1.53 KB

Forgot MetadataBag when adapting for changed Settings namespace. Also attaching the interdiff against #25.

YesCT’s picture

Issue summary: View changes
Issue tags: +Needs followup
znerol’s picture

Reroll after #2245003: Use a random seed instead of the session_id for CSRF token generation. I did not manage to generate a proper interdiff because of a merge conflict. The affected method is SessionManager::isEmptySession() (patch) which was introduced as SessionManager::isSessionObsolete() into HEAD by #2245003. Also introduced SessionManager::getSessionDataMask(), which helps with implementing SessionManager::isSessionObsolete() in a way which hopefully makes better sense (re @sun #7.2).

I think moving the csrf token seed to the metadata bag should be done in a follow-up. Therefore I consider this finished provided that the patch comes back green.

znerol’s picture

YesCT’s picture

FileSize
3.34 KB

this isn't a true interdiff. it's just a diff of the last two patches with some noise removed.

YesCT’s picture

+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -305,11 +351,29 @@ protected function isCli() {
+    $used_session_keys = array_filter($this->getSessionDataMask());
+    return empty($used_session_keys);

hm.

http://www.php.net/manual/en/function.array-filter.php

If no callback is supplied, all entries of array equal to FALSE (see converting to boolean) will be removed.

so isSessionObsolete() removes any keys that evaluate to false, and then checks if it's empty, and if it is, it is obsolete.

Are the docs on this method still ok? I looked and I think they are ok still.

YesCT’s picture

tiniest nit. just fixed it. (https://drupal.org/node/1354#functions third person verb, Returns instead of Return)

Next:
I'll open the followups.

YesCT’s picture

I created #2256257: Move token seed in SessionManager in isSessionObsolete() and regenerate() to the MetadataBag. removing the needs follow-up tag.

Seems like the two @todos that reference this issue (2238087) were the only two. And that they should be done in the same issue. (If not, please explain/create the other issue.)

YesCT’s picture

Issue summary: View changes
cosmicdreams’s picture

Status: Needs review » Reviewed & tested by the community

Looks good to me

catch’s picture

Status: Reviewed & tested by the community » Needs review

The title/issue summary confuses me a bit - given we don't actually want to use NativeSession Storage.

Is implementing the interface directly that bad?

If it is, then that seems like a good case for adding a BaseSessionStorage abstract class upstream, rather than extending a specific implementation.

sun’s picture

@catch: This issue is just one more "baby step" of the parent issue. The ultimate end goal is still to use all of Symfony's Session handling natively.

znerol’s picture

Issue summary: View changes
znerol’s picture

As discussed with @catch on IRC add documentation and @todo for the class.

cosmicdreams’s picture

Status: Needs review » Reviewed & tested by the community

Great comment, Still Good.

catch’s picture

Status: Reviewed & tested by the community » Fixed

Thanks! That makes it a lot more obvious what's going on.

Committed/pushed to 8.x.

  • Commit 37af509 on 8.x by catch:
    Issue #2238087 by znerol, YesCT: Rebase SessionManager onto Symfony...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.