Problem/Motivation

The session save handler currently is hard coded in the session manager constructor. Thus backends wishing to override session storage need to swap out session manager instead of the session save handler.

Note that Symfony also implements a proxy pattern for session handlers. This makes it possible to run code whenever a session is read/written without having to extend the save handler (and thus pollute it with non-storage related functionality). Examples of proxy classes are the WriteCheckSessionHandler which prevents unnecessary writes to the database and also #2338727: Replace static SessionManager::$enabled property with WriteSafeSessionHandler class and resolve hidden circular dependency between SessionManager and SessionHandler.

Proposed resolution

  • Register the SessionHandler as session_handler.storage service in the container.
  • Register session handler proxies in the container and tag them with session_handler_proxy
  • Implement a compiler pass similar to StackedKernelPass which collects the tagged proxies and wraps them around session_handler.storage. The compiler pass also points the alias session_handler to the service at the bottom of this stack.

Remaining tasks

User interface changes

API changes

Comments

znerol’s picture

Status: Active » Needs review
StatusFileSize
new7.53 KB
znerol’s picture

StatusFileSize
new7.89 KB
new15.42 KB

Adding tests.

znerol’s picture

StatusFileSize
new2.39 KB
new14.88 KB

Fix code-style in test, remove spurious @param annotation.

znerol’s picture

jhedstrom’s picture

+1 this looks great. Nice tests and everything.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/StackedSessionHandlerPass.php
    @@ -0,0 +1,51 @@
    +/**
    + * Provides a compiler pass for stacked session save handlers.
    + */
    +class StackedSessionHandlerPass implements CompilerPassInterface {
    +
    +  /**
    

    As talked yesterday, we should document/justify why its okay to create a pass for just a single session service by default.

  2. +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/StackedSessionHandlerPass.php
    @@ -0,0 +1,51 @@
    +
    +    $container->setAlias('session_handler', $decorated_id);
    

    It would be great to expose the session_handler somehow at least by default in the core.services.yml file, so tools like PHPStorm can do its autocompletion based upon it. Can't you point to some class by default?

  3. +++ b/core/lib/Drupal/Core/Session/SessionManager.php
    @@ -92,13 +90,7 @@ public function __construct(RequestStack $request_stack, Connection $connection,
    -    // Register the default session handler.
    -    // @todo Extract session storage from session handler into a service.
    -    $save_handler = new SessionHandler($this, $this->requestStack, $this->connection);
    -    $write_check_handler = new WriteCheckSessionHandler($save_handler);
    -    $this->setSaveHandler($write_check_handler);
    -
    -    parent::__construct($options, $write_check_handler, $metadata_bag);
    +    parent::__construct($options, NULL, $metadata_bag);
     
    

    +1 to no longer create the session handler automatically

  4. +++ b/core/modules/system/tests/modules/session_test/src/Session/TestSessionHandlerProxy.php
    @@ -0,0 +1,100 @@
    +   *   The decorated kernel.
    

    nitpick ... its not a decorated kernel.

znerol’s picture

StatusFileSize
new14.91 KB

Reroll. #6 still outstanding.

znerol’s picture

Fixes nitpicks from #6 (2,4).

Re #6.3: In fact it is not optimal to pass in NULL to the parent constructor, because then it simply instantiates its own session save handler. However, I was forced here to setter-injection because of the circular dependency between the session save handler and the session manager. Now that mixed mode ssl is gone, the only reason why this exists is the enabling/disabling session writes. As per #2338727: Replace static SessionManager::$enabled property with WriteSafeSessionHandler class and resolve hidden circular dependency between SessionManager and SessionHandler this should be moved into a session handler proxy anyway, therefore it might be reasonable to just do that in this issue.

znerol’s picture

StatusFileSize
new14.97 KB
new1.21 KB

This time with attachments.

Status: Needs review » Needs work

The last submitted patch, 9: 2372389-session-handler-stack-8.diff, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
StatusFileSize
new14.95 KB

Straight re-roll

Status: Needs review » Needs work

The last submitted patch, 14: 2372389-session-handler-stack-14.patch, failed testing.

almaudoh’s picture

Status: Needs work » Needs review
StatusFileSize
new14.95 KB
new1.48 KB

Fixed test fail due to class members' rename.

znerol’s picture

+++ b/core/modules/system/src/Tests/Session/StackSessionHandlerIntegrationTest.php
@@ -0,0 +1,52 @@
+      ['END', NULL, 'read', $this->sessionId  ],

Nitpick: There are spurious spaces before the closing bracket.

@almaudoh: Thank you very much for driving the session issues forward.

almaudoh’s picture

@znerol: No sweat. You did all the hard work :)

+++ b/core/lib/Drupal/Core/Session/SessionHandler.php
@@ -209,6 +206,22 @@ public function gc($lifetime) {
+   * Sets the session manager.
+   *
+   * @param \Drupal\Core\Session\SessionManagerInterface $session_manager
+   *   The session manager.
+   *
+   * @todo Remove dependency on session manager.
+   *   @see https://www.drupal.org/node/2342593
+   *   @see https://www.drupal.org/node/2338727
+   *
+   * @internal
+   */
+  public function setSessionManager(SessionManagerInterface $session_manager) {
+    $this->sessionManager = $session_manager;
+  }
+

The sessionManager variable is no longer used after #2338727: Replace static SessionManager::$enabled property with WriteSafeSessionHandler class and resolve hidden circular dependency between SessionManager and SessionHandler

almaudoh’s picture

StatusFileSize
new14.33 KB
new4.08 KB

Fixes #17 and #18 and adds the WriteSafeSessionHandler as a session handler proxy service.

cpj’s picture

Question about the tests - not sure if this is necessary, but does this simulate a stack with more than one member ?

almaudoh’s picture

@cpj: the test simulates a two member stack and verifies that the higher priority member is called before the lower priority one, as designed.

cpj’s picture

@almaudoh - thanks. I've reviewed and tested the code as far as I can, meaning it runs in our development environment, which is Beta-6 plus other relevant recent patches, and I've stepped through it in the debugger for a simple, non-multi-level stack. I couldn't get HEAD to run yesterday, but I that didn't appear to be related to this patch. Is that sufficient for RTBC ?

cpj’s picture

Status: Needs review » Reviewed & tested by the community
alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -83,20 +82,17 @@ class SessionManager extends NativeSessionStorage implements SessionManagerInter
+   * @param \Drupal\Core\Session\WriteSafeSessionHandlerInterface $write_safe_handler
+   *   The session handler proxy that allows session writes to be disabled.
...
+  public function __construct(RequestStack $request_stack, Connection $connection, MetadataBag $metadata_bag, SessionConfigurationInterface $session_configuration, WriteSafeSessionHandlerInterface $write_safe_handler) {
...
+    $this->writeSafeHandler = $write_safe_handler;

Given that the plan is to not have the write safe handler in the session manger how about extracting the write safe handler in the setSaveHandler method? That way we have less constructor change.

almaudoh’s picture

Status: Needs work » Needs review
StatusFileSize
new13.9 KB
new2.46 KB

Addressed #24

almaudoh’s picture

+++ b/core/lib/Drupal/Core/Session/SessionManager.php
@@ -289,6 +286,19 @@ public function enable() {
+    else {
+      $this->writeSafeHandler = \Drupal::service('session_handler.write_safe');
+    }

Not sure if this is the best way, or whether to allow the exception if null is passed in.

This option caters for the case where another session proxy (higher priority) is added by some other module.

cpj’s picture

I like the idea to use the setter method for the handler rather than passing it in via the constructor. But I don't understand the need for the hard-coded fall back. Why would the presence of a higher priority session proxy trigger the setter to be called with null ?

znerol’s picture

StatusFileSize
new5.06 KB
new16.88 KB

I took another look at the issue. First off, the reason why this was postponed on #2338727: Replace static SessionManager::$enabled property with WriteSafeSessionHandler class and resolve hidden circular dependency between SessionManager and SessionHandler was the cyclic dependency. Before the other issue went in, it was only possible to inject the session handler into the session manager via setter-injection. However, the base class of the session manager (Symfonys NativeSessionStorage) will instantiate and register its own session handler if we do not pass anything to its constructor. Even though it would be replaced immediately by the setSaveHandler() call, that behavior is not desirable at all.

Therefore we really want to inject the session handler into the constructor of the session manager. I do not have any preference regarding the write-safe handler, thus going with #24 (i.e. setter injection) for that.

Note that I've added deprecations to SessionManagerInterface isEnabled(), enable() and disable() methods. Calls to those methods should be replaced by the respective methods of the write-safe handler. A quick grep through the source tree turned up the following files which need to be updated:

core/lib/Drupal/Core/Session/AccountSwitcher.php
core/modules/system/src/Tests/Datetime/DrupalDateTimeTest.php
core/modules/system/src/Tests/Session/AccountSwitcherTest.php
core/modules/system/src/Tests/Session/SessionTest.php
core/modules/system/tests/modules/session_test/src/Controller/SessionTestController.php
core/modules/user/src/Tests/Views/ArgumentDefaultTest.php
almaudoh’s picture

@cpj: sorry I wasn't very clear in my earlier comment. A higher priority session proxy handler would mean the setter is called not with null, but an object that doesn't implement WriteSafeSessionHandlerInterface hence the setSessionWritable() method call would fail.

The fallback would allow us to still disable session writes since the higher priority proxy may still write through the write-safe handler.

But as has already been stated, this would only be temporary since the writeSafeHandler will be removed from SessionManager in a later refactoring.

almaudoh’s picture

Raised follow-up #2426031: Remove deprecated uses of SessionManager::isEnabled(), SessionManager::enable() and SessionManager::disable() to remove deprecated methods. We need someone to RTBC #28 so we can meet the next beta-release window. Anyone??

cpj’s picture

I tested #19 pretty thoroughly in #22. Reviewing the interdiffs for #25 and #28, I understand the changes to remove then add back the session injection, so as far as I can see #28 is very similar to #19, right ? Therefore I'm gonna set this as RTBC. OK ?

cpj’s picture

Status: Needs review » Reviewed & tested by the community

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 28: 372389-session-handler-stack-28.diff, failed testing.

almaudoh’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new16.91 KB
alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue is a major task that is part of on-going efforts to reduce fragility and improve extensibility of the session system by using Symfony's session components. Per https://www.drupal.org/core/beta-changes, this is a good change to complete during the Drupal 8 beta phase. Committed a32a88c and pushed to 8.0.x. Thanks!

  • alexpott committed a32a88c on 8.0.x
    Issue #2372389 by znerol, almaudoh: Expose session handler in container
    
almaudoh’s picture

Thanks @alexpott. Now on to #2426031: Remove deprecated uses of SessionManager::isEnabled(), SessionManager::enable() and SessionManager::disable() which will remove the deprecated uses of SessionManager::enable(), ::disable(), etc.

Status: Fixed » Closed (fixed)

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