Commit 22bb5489 authored by Sergey Kolosov's avatar Sergey Kolosov Committed by Tim Graham
Browse files

Fixed #22634 -- Made the database-backed session backends more extensible.

Introduced an AbstractBaseSession model and hooks providing the option
of overriding the model class used by the session store and the session
store class used by the model.
parent 956df84a
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -640,6 +640,7 @@ answer newbie questions, and generally made Django that much better:
    Sengtha Chay <sengtha@e-khmer.com>
    Senko Rašić <senko.rasic@dobarkod.hr>
    serbaut@gmail.com
    Sergey Kolosov <m17.admin@gmail.com>
    Seth Hill <sethrh@gmail.com>
    Shai Berger <shai@platonix.com>
    Shannon -jj Behrens <http://jjinux.blogspot.com/>
+5 −3
Original line number Diff line number Diff line
@@ -10,13 +10,15 @@ class SessionStore(SessionBase):
    """
    A cache-based session store.
    """
    cache_key_prefix = KEY_PREFIX

    def __init__(self, session_key=None):
        self._cache = caches[settings.SESSION_CACHE_ALIAS]
        super(SessionStore, self).__init__(session_key)

    @property
    def cache_key(self):
        return KEY_PREFIX + self._get_or_create_session_key()
        return self.cache_key_prefix + self._get_or_create_session_key()

    def load(self):
        try:
@@ -62,14 +64,14 @@ class SessionStore(SessionBase):
            raise CreateError

    def exists(self, session_key):
        return session_key and (KEY_PREFIX + session_key) in self._cache
        return session_key and (self.cache_key_prefix + session_key) in self._cache

    def delete(self, session_key=None):
        if session_key is None:
            if self.session_key is None:
                return
            session_key = self.session_key
        self._cache.delete(KEY_PREFIX + session_key)
        self._cache.delete(self.cache_key_prefix + session_key)

    @classmethod
    def clear_expired(cls):
+6 −9
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ class SessionStore(DBStore):
    """
    Implements cached, database backed sessions.
    """
    cache_key_prefix = KEY_PREFIX

    def __init__(self, session_key=None):
        self._cache = caches[settings.SESSION_CACHE_ALIAS]
@@ -25,7 +26,7 @@ class SessionStore(DBStore):

    @property
    def cache_key(self):
        return KEY_PREFIX + self._get_or_create_session_key()
        return self.cache_key_prefix + self._get_or_create_session_key()

    def load(self):
        try:
@@ -39,14 +40,14 @@ class SessionStore(DBStore):
            # Duplicate DBStore.load, because we need to keep track
            # of the expiry date to set it properly in the cache.
            try:
                s = Session.objects.get(
                s = self.model.objects.get(
                    session_key=self.session_key,
                    expire_date__gt=timezone.now()
                )
                data = self.decode(s.session_data)
                self._cache.set(self.cache_key, data,
                    self.get_expiry_age(expiry=s.expire_date))
            except (Session.DoesNotExist, SuspiciousOperation) as e:
            except (self.model.DoesNotExist, SuspiciousOperation) as e:
                if isinstance(e, SuspiciousOperation):
                    logger = logging.getLogger('django.security.%s' %
                            e.__class__.__name__)
@@ -56,7 +57,7 @@ class SessionStore(DBStore):
        return data

    def exists(self, session_key):
        if session_key and (KEY_PREFIX + session_key) in self._cache:
        if session_key and (self.cache_key_prefix + session_key) in self._cache:
            return True
        return super(SessionStore, self).exists(session_key)

@@ -70,7 +71,7 @@ class SessionStore(DBStore):
            if self.session_key is None:
                return
            session_key = self.session_key
        self._cache.delete(KEY_PREFIX + session_key)
        self._cache.delete(self.cache_key_prefix + session_key)

    def flush(self):
        """
@@ -80,7 +81,3 @@ class SessionStore(DBStore):
        self.clear()
        self.delete(self.session_key)
        self._session_key = None


# At bottom to avoid circular import
from django.contrib.sessions.models import Session  # isort:skip
+33 −16
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ from django.core.exceptions import SuspiciousOperation
from django.db import IntegrityError, router, transaction
from django.utils import timezone
from django.utils.encoding import force_text
from django.utils.functional import cached_property


class SessionStore(SessionBase):
@@ -14,14 +15,25 @@ class SessionStore(SessionBase):
    def __init__(self, session_key=None):
        super(SessionStore, self).__init__(session_key)

    @classmethod
    def get_model_class(cls):
        # Avoids a circular import and allows importing SessionStore when
        # django.contrib.sessions is not in INSTALLED_APPS.
        from django.contrib.sessions.models import Session
        return Session

    @cached_property
    def model(self):
        return self.get_model_class()

    def load(self):
        try:
            s = Session.objects.get(
            s = self.model.objects.get(
                session_key=self.session_key,
                expire_date__gt=timezone.now()
            )
            return self.decode(s.session_data)
        except (Session.DoesNotExist, SuspiciousOperation) as e:
        except (self.model.DoesNotExist, SuspiciousOperation) as e:
            if isinstance(e, SuspiciousOperation):
                logger = logging.getLogger('django.security.%s' %
                        e.__class__.__name__)
@@ -30,7 +42,7 @@ class SessionStore(SessionBase):
            return {}

    def exists(self, session_key):
        return Session.objects.filter(session_key=session_key).exists()
        return self.model.objects.filter(session_key=session_key).exists()

    def create(self):
        while True:
@@ -45,6 +57,18 @@ class SessionStore(SessionBase):
            self.modified = True
            return

    def create_model_instance(self, data):
        """
        Return a new instance of the session model object, which represents the
        current session state. Intended to be used for saving the session data
        to the database.
        """
        return self.model(
            session_key=self._get_or_create_session_key(),
            session_data=self.encode(data),
            expire_date=self.get_expiry_date(),
        )

    def save(self, must_create=False):
        """
        Saves the current session data to the database. If 'must_create' is
@@ -54,12 +78,9 @@ class SessionStore(SessionBase):
        """
        if self.session_key is None:
            return self.create()
        obj = Session(
            session_key=self._get_or_create_session_key(),
            session_data=self.encode(self._get_session(no_load=must_create)),
            expire_date=self.get_expiry_date()
        )
        using = router.db_for_write(Session, instance=obj)
        data = self._get_session(no_load=must_create)
        obj = self.create_model_instance(data)
        using = router.db_for_write(self.model, instance=obj)
        try:
            with transaction.atomic(using=using):
                obj.save(force_insert=must_create, using=using)
@@ -74,14 +95,10 @@ class SessionStore(SessionBase):
                return
            session_key = self.session_key
        try:
            Session.objects.get(session_key=session_key).delete()
        except Session.DoesNotExist:
            self.model.objects.get(session_key=session_key).delete()
        except self.model.DoesNotExist:
            pass

    @classmethod
    def clear_expired(cls):
        Session.objects.filter(expire_date__lt=timezone.now()).delete()


# At bottom to avoid circular import
from django.contrib.sessions.models import Session  # isort:skip
        cls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete()
+51 −0
Original line number Diff line number Diff line
"""
This module allows importing AbstractBaseSession even
when django.contrib.sessions is not in INSTALLED_APPS.
"""
from __future__ import unicode_literals

from django.db import models
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _


class BaseSessionManager(models.Manager):
    def encode(self, session_dict):
        """
        Return the given session dictionary serialized and encoded as a string.
        """
        session_store_class = self.model.get_session_store_class()
        return session_store_class().encode(session_dict)

    def save(self, session_key, session_dict, expire_date):
        s = self.model(session_key, self.encode(session_dict), expire_date)
        if session_dict:
            s.save()
        else:
            s.delete()  # Clear sessions with no data.
        return s


@python_2_unicode_compatible
class AbstractBaseSession(models.Model):
    session_key = models.CharField(_('session key'), max_length=40, primary_key=True)
    session_data = models.TextField(_('session data'))
    expire_date = models.DateTimeField(_('expire date'), db_index=True)

    objects = BaseSessionManager()

    class Meta:
        abstract = True
        verbose_name = _('session')
        verbose_name_plural = _('sessions')

    def __str__(self):
        return self.session_key

    @classmethod
    def get_session_store_class(cls):
        raise NotImplementedError

    def get_decoded(self):
        session_store_class = self.get_session_store_class()
        return session_store_class().decode(self.session_data)
Loading