view MoinMoin/session.py @ 3426:79dd730d6455

common.css: added yellow and orange as background color
author Reimar Bauer <rb.proj AT googlemail DOT com>
date Sat, 29 Mar 2008 13:07:53 +0100
parents 4cd85d841bbd
children 9a3deab96cb7
line wrap: on
line source
"""
    MoinMoin - session handling

    Session handling in MoinMoin is done mostly by the request
    with help from a SessionHandler instance (see below.)


    @copyright: 2007      MoinMoin:JohannesBerg

    @license: GNU GPL, see COPYING for details.
"""

import Cookie
from MoinMoin import caching
from MoinMoin.user import User
from MoinMoin.util import random_string
import time, random

class SessionData(object):
    """
        MoinMoin session data base class

        An object of this class must be assigned to
        request.session by the SessionHandler's start
        method.

        Instances conform to the dict protocol (__setitem__, __getitem__,
        __contains__, __delitem__, get) and have the additional properties
        is_stored and is_new.
    """
    def __init__(self, request):
        self.is_stored = False
        self.is_new = True
        self.request = request

    def __setitem__(self, name, value):
        raise NotImplementedError

    def __getitem__(self, name):
        raise NotImplementedError

    def __contains__(self, name):
        raise NotImplementedError

    def __delitem__(self, name):
        raise NotImplementedError

    def get(self, name, default=None):
        raise NotImplementedError


class DefaultSessionData(SessionData):
    """ DefaultSessionData -- session data for DefaultSessionHandler

    If you wish to override just the session storage then you can
    inherit from this class, implement all methods and assign the
    class to the dataclass keyword parameter to the DefaultSessionHandler
    constructor.

    Newly created objects should have be marked as expiring right away
    until set_expiry() is called.
    """
    def __init__(self, request, name):
        """create session object

        @param request: the request
        @param name: the session name
        """
        SessionData.__init__(self, request)
        self.name = name

    def set_expiry(self, expires):
        """reset expiry for this session object"""
        raise NotImplementedError

    def delete(self):
        """clear session data and remove from it storage"""
        raise NotImplementedError

    def cleanup(cls, request):
        """clean up expired sessions"""
        raise NotImplementedError
    cleanup = classmethod(cleanup)

class CacheSessionData(DefaultSessionData):
    """ SessionData -- store data for a session

    This stores session data in memory and also maintains a cache of it on
    disk, so the same data will be loaded from disk cache in the next request
    of the same session.

    Once in a while, expired session's cache files will be automatically cleaned up.
    """
    def __init__(self, request, name):
        DefaultSessionData.__init__(self, request, name)

        # we can use farm scope since the session name is totally random
        # this means that the session is kept over multiple wikis in a farm
        # when they share user_dir and cookies
        self._ce = caching.CacheEntry(request, 'session', name, 'farm',
                                      use_pickle=True)
        try:
            self._data = self._ce.content()
            if self['expires'] <= time.time():
                self._ce.remove()
                self._data = {'expires': 0}
        except caching.CacheError:
            self._data = {'expires': 0}

    def __setitem__(self, name, value):
        self._data[name] = value
        if len(self._data) > 1 and self['expires'] > time.time():
            self._ce.update(self._data)

    def __getitem__(self, name):
        return self._data[name]

    def __contains__(self, name):
        return name in self._data

    def __delitem__(self, name):
        del self._data[name]
        if len(self._data) <= 1:
            self._ce.remove()
        elif self['expires'] > time.time():
            self._ce.update(self._data)

    def get(self, name, default=None):
        return self._data.get(name, default)

    def set_expiry(self, expires):
        # Set 'expires' an hour later than it should actually expire.
        # That way, the expiry code will delete the item an hour later
        # than it has actually expired, but that is acceptable and we
        # don't need to update the file all the time
        if expires and self['expires'] < expires:
            self['expires'] = expires + 3600

    def delete(self):
        try:
            self._ce.remove()
        except caching.CacheError:
            pass
        self._data = {'expires': 0}

    def cleanup(cls, request):
        cachelist = caching.get_cache_list(request, 'session', 'farm')
        tnow = time.time()
        for name in cachelist:
            entry = caching.CacheEntry(request, 'session', name, 'farm',
                                       use_pickle=True)
            try:
                data = entry.content()
                if 'expires' in data and data['expires'] < tnow:
                    entry.remove()
            except caching.CacheError:
                pass
    cleanup = classmethod(cleanup)


class SessionHandler(object):
    """
        MoinMoin session handler base class

        SessionHandler is an abstract method defining the interface
        to a session handler object.

        Session handling in MoinMoin works as follows:

        When a request is received, first the cookie is read into a
        Cookie.SimpleCookie instance, this is passed to the selected
        session handler's (cfg.session_handler) start method (see below)
        which must return a MoinMoin.user.User instance (or None). The
        session handler is also responsible for string the user object's
        auth_method and auth_attribs fields across sessions as those are
        not saved to the user file.

        Then, all authentication methods are called with this user object,
        they can modify it or return a different one.

        After they have changed the user object suitably, the session
        handler's after_auth method is invoked to set the cookie.

        Then, the request is executed and finally the session handler's
        finish method is invoked.
    """
    def __init__(self):
        """
           Session handler initialisation

           Only provided for future compatibility.
        """
        pass

    def start(self, request, cookie):
        """
           Session handler start

           Invoked very early during request handling to preload
           a user object from the session (if any.)
           This method must also assign to request.session an object
           derived from SessionDataInterface.

           @param request: the request instance
           @param cookie: a Cookie.SimpleCookie with the request cookie
           @return: a MoinMoin.user.User instance or None
        """
        raise NotImplementedError

    def after_auth(self, request, cookie, user_obj):
        """
           Session handler auth chain callback

           Invoked after all auth items have run (or multistage was
           requested by one), but before the request is actually
           handled and output is made. Should set the cookie.

           @param request: the request instance
           @param cookie: a Cookie.SimpleCookie with the request cookie
           @param user_obj: the user object returned from the auth methods
                            (or None)
        """
        raise NotImplementedError

    def finish(self, request, cookie, user_obj):
        """
           Session handler request finish callback

           Invoked after the request is completely finished.

           @param request: the request instance
           @param cookie: a Cookie.SimpleCookie with the request cookie
           @param user_obj: the user object that was used in this request
        """
        raise NotImplementedError




class SessionIDHandler:
    """
        MoinMoin session ID handling

        Instances of this class are used by the session handling code
        to set/get the persistent ID that is used to identify the session
        which is usually stored in a cookie.
    """
    _SESSION_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_-'
    _SESSION_NAME_LEN = 32

    def __init__(self):
        """
            Initialise the session ID handler.
        """
        pass

    def get(self, request):
        """
            Return the persistent ID for this request.

            @param request: the request instance
        """
        raise NotImplementedError

    def set(self, request, session_id, expires):
        """
            Set a persistent ID for the request to be returned by the
            user agent.

            @param request: the request instance
            @param session_id: the ID for this session
            @param expires: expiry date/time in unix seconds (cf. time.time())
        """
        raise NotImplementedError

    def generate_new_id(self, request):
        """
            Generate a new unique ID.

            @param request: the request instance
        """
        return random_string(self._SESSION_NAME_LEN, self._SESSION_NAME_CHARS)



class MoinCookieSessionIDHandler(SessionIDHandler):
    def __init__(self, cookie_name='MOIN_SESSION'):
        SessionIDHandler.__init__(self)
        self.cookie_name = cookie_name

    def _make_cookie(self, request, cookie_name, cookie_string, maxage, expires):
        """ create an appropriate cookie """
        cookie = Cookie.SimpleCookie()
        cfg = request.cfg
        cookie[cookie_name] = cookie_string
        cookie[cookie_name]['max-age'] = maxage
        if cfg.cookie_domain:
            cookie[cookie_name]['domain'] = cfg.cookie_domain
        if cfg.cookie_path:
            cookie[cookie_name]['path'] = cfg.cookie_path
        else:
            path = request.getScriptname()
            if not path:
                path = '/'
            cookie[cookie_name]['path'] = path
        # Set expires for older clients
        cookie[cookie_name]['expires'] = request.httpDate(when=expires, rfc='850')
        return cookie.output()

    def _set_cookie(self, request, cookie_string, expires):
        """ Set cookie, raw helper. """
        lifetime = expires - time.time()
        cookie = self._make_cookie(request, self.cookie_name, cookie_string,
                                   lifetime, expires)
        # Set cookie
        request.setHttpHeader(cookie)
        # IMPORTANT: Prevent caching of current page and cookie
        request.disableHttpCaching()

    def set(self, request, session_name, expires):
        """ Set moin_session cookie """
        self._set_cookie(request, session_name, expires)

    def get(self, request):
        session_name = None
        if request.cookie and self.cookie_name in request.cookie:
            session_name = request.cookie[self.cookie_name].value
            session_name = ''.join([c for c in session_name
                                    if c in self._SESSION_NAME_CHARS])
            session_name = session_name[:self._SESSION_NAME_LEN]
        return session_name


def _get_anon_session_lifetime(request):
    if hasattr(request.cfg, 'anonymous_session_lifetime'):
        return request.cfg.anonymous_session_lifetime * 3600
    return 0

def _get_session_lifetime(request, user_obj):
    """ Get session lifetime for the user object user_obj """
    lifetime = int(request.cfg.cookie_lifetime) * 3600
    forever = 10 * 365 * 24 * 3600 # 10 years
    if not lifetime:
        return forever
    elif lifetime > 0:
        if user_obj.remember_me:
            return forever
        return lifetime
    elif lifetime < 0:
        return -lifetime
    return lifetime


class DefaultSessionHandler(SessionHandler):
    """MoinMoin default session handler

    This session handler uses the MOIN_SESSION cookie and a configurable
    session data class.
    """
    def __init__(self, dataclass=CacheSessionData):
        """DefaultSessionHandler constructor

        @param dataclass: class derived from DefaultSessionData or a callable
                          that takes parameters (request, name, expires)
                          and returns a DefaultSessionData instance.
        """
        SessionHandler.__init__(self)
        self.dataclass = dataclass

    def start(self, request, session_id_handler):
        user_obj = None
        session_name = session_id_handler.get(request)
        if session_name:
            sessiondata = self.dataclass(request, session_name)
            sessiondata.is_new = False
            sessiondata.is_stored = True
            request.session = sessiondata
            if 'user.id' in sessiondata:
                uid = sessiondata['user.id']
                method = sessiondata['user.auth_method']
                attribs = sessiondata['user.auth_attribs']
                # Only allow valid methods that are still in the auth list.
                # This is necessary to kick out clients who authenticated in
                # the past with a method that was removed from the auth list!
                if method:
                    for auth in request.cfg.auth:
                        if auth.name == method:
                            user_obj = User(request, id=uid,
                                            auth_method=method,
                                            auth_attribs=attribs)
                            if user_obj:
                                sessiondata.is_stored = True
            else:
                store = hasattr(request.cfg, 'anonymous_session_lifetime')
                sessiondata.is_stored = store
        else:
            session_name = session_id_handler.generate_new_id(request)
            store = hasattr(request.cfg, 'anonymous_session_lifetime')
            sessiondata = self.dataclass(request, session_name)
            sessiondata.is_new = True
            sessiondata.is_stored = store
            request.session = sessiondata
        return user_obj

    def after_auth(self, request, session_id_handler, user_obj):
        session = request.session
        if user_obj and user_obj.valid:
            session['user.id'] = user_obj.id
            session['user.auth_method'] = user_obj.auth_method
            session['user.auth_attribs'] = user_obj.auth_attribs
            lifetime = _get_session_lifetime(request, user_obj)
            expires = time.time() + lifetime
            session_id_handler.set(request, session.name, expires)
            request.session.set_expiry(expires)
        else:
            if 'user.id' in session:
                session.delete()
            lifetime = _get_anon_session_lifetime(request)
            if lifetime:
                expires = time.time() + lifetime
                session_id_handler.set(request, session.name, expires)
                request.session.set_expiry(expires)
            else:
                session.delete()

    def finish(self, request, session_id_handler, user_obj):
        # every once a while, clean up deleted sessions:
        if random.randint(0, 999) == 0:
            self.dataclass.cleanup(request)