"""This module contains the base classes SessionContainer and Session."""
__docformat__ = "restructuredtext"
# Created: Tue Mar 23 13:09:56 PST 2004
# Author: Shannon -jj Behrens
# Email: jjinux@users.sourceforge.net
#
# Copyright (c) Shannon -jj Behrens. All rights reserved.
from random import choice
import time
import aquarium.conf.AquariumProperties as properties
try:
from threading import Lock
except ImportError:
class Lock:
"""Python was built without thread support. I'll use a fake Lock.
My assumption is that a subclass is going to create its own Lock. I
wish I could just create a Lock factory in ``SessionContainer``, but
the Lock must be initialized statically. This is the least ugly HACK I
could think of.
"""
def acquire(self): pass
def release(self): pass
class SessionContainer:
"""This is a container for sessions.
A container class is needed because:
* We need to be able to call certain methods such as ``cleanup`` without
actually creating a ``Session`` instance. Hence, these methods obviously
can't be instance methods of the ``Session`` class.
* I want subclasses to be able to override some of these methods.
On the other hand, no instance state is kept, so one instance of this class
is just as good as another. All state is kept in the class. I'd use a
singleton, but the version of Python I'm stuck with doesn't have class
methods.
Concerning locking: in general, a global lock (of some sort) should be
used so that creating, deleting, reading, and writing sessions is
serialized. However, it is not necessary to have a lock for each session.
If a user wishes to use two browser windows at the same time, the last
writer wins.
Concerning aquarium.util.AquariumClass_: this class does not subclass
aquarium.util.AquariumClass and does not require a ``ctx`` parameter in its
constructor. Subclasses may choose to mixin aquarium.util.AquariumClass
as necessary, but they will then require a ``ctx`` in their constructors.
aquarium.util.Aquarium_ is aware of this.
This base class is useful for mutlithreaded environments. For other
environments, such as CGI, it simply provides an API.
The following class level constants are defined:
SID_LENGTH
This is the length of standard sid's.
These are needed since not all "mutexes" provide the same API,
unfortunately:
_acquire
Acquire the global lock.
_release
Release the global lock.
The following protected class variables are used:
_lock
This is the global lock. It is initialized statically. (This base class
uses ``threading.Lock``. Subclasses can set this to something different
if appropriate.)
_sessions
This is the global dictionary of sessions.
.. _aquarium.util.AquariumClass:
aquarium.util.AquariumClass.AquariumClass-class.html
.. _aquarium.util.Aquarium:
aquarium.util.Aquarium.Aquarium-class.html
"""
SID_LENGTH = 20
_lock = Lock()
_sessions = {}
def open(self, sid=None):
"""Create or open a session.
sid
If this is None, the default, or if no such session exists, a
new sid will be generated (even if you provided an sid) and a new
session will be created with that sid.
Return the session.
"""
self._acquire()
try:
if not sid or not self._exists(sid):
tries = 10
while 1:
sid = self._createSid()
if not self._exists(sid):
break
tries -= 1
if tries <= 0:
raise OverflowError("Could not create an unused sid")
self._sessions[sid] = self._createSession(sid)
return self._sessions[sid]
finally:
self._release()
def cleanup(self):
"""Delete all of the expired sessions.
It's the application's responsibility to occasionally call this.
"""
self._acquire()
try:
unlinkThese = [ # Don't modify the dict during iteration.
sid
for sid in self._sessions
if self._isExpired(sid)
]
map(self._unlink, unlinkThese)
finally:
self._release()
def adjustTime(self, deltaSeconds):
"""Adjust all of the ``lastModified`` keys.
If you have to change the time, do this::
sessionContainer._acquire()
try:
changeTime()
sessionContainer.adjustTime(deltaSeconds)
finally:
sessionContainer._release()
You have to manage the lock yourself, otherwise changing the time might
cause some sessions to get deleted before we can update them.
"""
for sid in self._sessions.keys():
self._sessions[sid]["lastModified"] += deltaSeconds
def _createSid(self):
"""Generate a new sid.
It is not guaranteed to be unique.
"""
pool = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890"
return "".join([choice(pool) for _unused in range(self.SID_LENGTH)])
def _createSession(self, sid):
"""This is a session factory method.
It's here so that you can override what type of session gets
created without too much trouble. You must acquire the lock before
calling this.
"""
return Session(sid)
def _exists(self, sid):
"""Does a session with the given sid exist?
This implies that it is not expired. You must acquire the lock before
calling this.
"""
return self._sessions.has_key(sid) and not self._isExpired(sid)
def _isExpired(self, sid):
"""Is the session with the given sid expired?
If the session doesn't exist, this will raise a ``KeyError``. You must
acquire the lock before calling this.
"""
return (self._sessions[sid]["lastModified"] <
time.time() - properties.MAXIMUM_SESSION_LIFETIME)
def _unlink(self, sid):
"""Delete a session permanently.
You must acquire the lock before calling this.
"""
del self._sessions[sid]
def _acquire(self):
"""Acquire the global lock."""
self._lock.acquire()
def _release(self):
"""Release the global lock."""
self._lock.release()
class Session(dict):
"""This is a dict-like object for session management.
The following keys are used:
sid
This is the session's ID.
lastModified
This is the last time the session was modified.
"""
def __init__(self, sid):
"""Create or open a session.
Don't call this directly--use ``SessionContainer.open``.
"""
dict.__init__(self)
self["sid"] = sid
self["lastModified"] = time.time()
def save(self):
"""Persist a session.
This method may be extended. This base class takes care of updating
the ``lastModified`` key.
"""
self["lastModified"] = time.time()
def clear(self):
"""Clear everything except the ``sid`` and ``lastModified`` keys."""
sid = self["sid"]
dict.clear(self)
self["sid"] = sid
self["lastModified"] = time.time()
def getCookieExpiration(self):
"""Return a value for ``ctx.response.cookie["sid"]["expires"]``.
If ``properties.SET_COOKIE_EXPIRATION`` is 0 or not set, just return 0.
Otherwise, return something based on
``properties.MAXIMUM_SESSION_LIFETIME``.
"""
if not getattr(properties, "SET_COOKIE_EXPIRATION", 0):
return 0
else:
expires = time.gmtime(time.time() +
properties.MAXIMUM_SESSION_LIFETIME)
return time.strftime("%A, %d-%b-%Y %T GMT", expires)
def getCookieExpirationNever():
"""Return this in ``Session.getCookieExpiration`` for a long-lasting cookie.
Sometimes you don't want the cookie to go away when the user closes his
browser. Setting a fixed time limit is bad if your software allows the
user to change the server's time. Neither ``expires`` nor ``Max-Age``
works for this case. Hence, I'll just return something 20 years from now.
I see that Google, PayPal, and MSN are doing something similar. If the
user sets his time wrong by more than 20 years, he'll have to change his
browser's time too.
"""
LONG_TIME = 60 * 60 * 24 * 365 * 20
expires = time.gmtime(time.time() + LONG_TIME)
return time.strftime("%A, %d-%b-%Y %T GMT", expires)
|