[Zope3-checkins]
SVN: Zope3/branches/stub-session/src/zope/app/session/
Session refactoring work
Stuart Bishop
zen at shangri-la.dropbear.id.au
Mon Jul 5 17:25:38 EDT 2004
Log message for revision 26101:
Session refactoring work
- Shuffled and improved tests, including using the API documentation
as a doctest suite.
- Improved documentation, including additional docs
from branches/jim-session
- Started merging ideas from branches/jim-session
- browser id is now client id
- design and rationale documentation
- Fix session expiry code
-=-
Copied: Zope3/branches/stub-session/src/zope/app/session/README.txt (from rev 26094, Zope3/branches/jim-session/src/zope/app/session/README.txt)
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/README.txt 2004-07-03 12:03:40 UTC (rev 26094)
+++ Zope3/branches/stub-session/src/zope/app/session/README.txt 2004-07-05 21:25:38 UTC (rev 26101)
@@ -0,0 +1,148 @@
+Sessions
+========
+
+Sessions provide a way to temporarily associate information with a
+client without requiring the authentication of a principal. We
+associate an identifier with a particular client. Whenever we get a
+request from that client, we compute the identifier and use the
+identifier to look up associated information, which is stored on the
+server.
+
+A major disadvantage of sessions is that they require management of
+information on the server. This can have major implications for
+scalability. It is possible for a framework to make use of session
+data very easy for the developer. This is great if scalability is not
+an issue, otherwise, it is a booby trap.
+
+Design Issues
+-------------
+
+Sessions introduce a number of issues to be considered:
+
+- Clients have to be identified. A number of approaches are possible,
+ including:
+
+ o Using HTTP cookies. The application assigns a client identifier,
+ which is stored in a cookie. This technique is the most
+ straightforward, but can be defeated if the client does not
+ support HTTP cookies (usually because the feature has been
+ disabled).
+
+ o Using URLs. The application assigns a client identifier, which is
+ stored in the URL. This makes URLs a bit uglier and requires some
+ care. If people copy URLs and send them to others, then you could
+ end up with multiple clients with the same session
+ identifier. There are a number of ways to reduce the risk of
+ accidental reuse of session identifiers:
+
+ - Embed the client IP address in the identifier
+
+ - Expire the identifier
+
+ o Use hidden form variables. This complicates applications. It
+ requires all requests to be POST requests and requires the
+ maintenance of the hidden variables.
+
+ o Use the client IP address
+
+ This doesn't work very well, because an IP address may be shared by
+ many clients.
+
+- Data storage
+
+ Data can be simply stored in the object database. This provides lots
+ of flexibility. You can store pretty much anything you want as long
+ as it is persistent. You get the full benefit of the object database,
+ such as transactions, transparency, clustering, and so on. Using
+ the object database is especially useful when:
+
+ - Writes are infrequent
+
+ - Data are complex
+
+ If writes are frequent, then the object database introduces
+ scalability problems. Really, any transactional database is likely
+ to introduce problems with frequent writes. If you are tempted to
+ update session data on every request, think very hard about it. You
+ are creating a scalability problem.
+
+ If you know that scalability is not (and never will be) an issue,
+ you can just use the object database.
+
+ If you have client data that needs to be updated often (as in every
+ request), consider storing the data on the client. (Like all data
+ received from a client, it may be tainted and, in most instances,
+ should not be trusted. Sensitive information that the user should
+ not see should likewise not be stored on the client, unless
+ encrypted with a key the client has no access to.) If you can't
+ store it on the client, then consider some other storage mechanism,
+ like a fast database, possibly without transaction support.
+
+ You may be tempted to store session data in memory for speed. This
+ doesn't turn out to work very well. If you need scalability, then
+ you need to be able to use an application-server cluster and storage
+ of session data in memory defeats that. You can use
+ "server-affinity" to assure that requests from a client always go
+ back to the same server, but not all load balancers support server
+ affinity, and, for those that do, enabling server affinity tends to
+ defeat load balancing.
+
+- Session expiration
+
+ You may wish to ensure that sessions terminate after some period of
+ time. This may be for security reasons, or to avoid accidental
+ sharing of a session among multiple clients. The policy might be
+ expressed in terms of total session time, or maximum inactive time,
+ or some combination.
+
+ There are a number of ways to approach this. You can expire client
+ ids. You can expire session data.
+
+- Data expiration
+
+ Because HTTP is a stateless protocol, you can't tell whether a user
+ is thinking about a task or has simply stopped working on it. Some
+ means is needed to free server session storage that is no-longer needed.
+
+ The simplest strategy is to never remove data. This strategy has
+ some obvious disadvantages. Other strategies can be viewed as
+ optimizations of the basic strategy. It is important to realize that
+ a data expiration strategy can be informed by, but need not be
+ constrained by a session-expiration strategy.
+
+Application programming interface
+---------------------------------
+
+Application code will merely adapt request objects to a session data
+interface. Initially, we will define the session data interface
+`IPersistentSessionData'. `IPersistentSessionData` provides a mapping
+interface. Keys in the mapping are application identifiers. Values are
+persistent mapping objects.
+
+Application code that wants to get object session data for a request
+adapts the request to `IPersistentSessionData`::
+
+ data = IPersistentSessionData(request)[appkey]
+
+where `appkey` is a dotted name that identifies the application, such
+as ``zope.app.actionplan``. Given the session data, the application
+can then store data in it::
+
+ data['original'] = original_actions
+ data['new'] = new_actions
+
+From ZPT, you can access session data using adapter syntax::
+
+ request*PersistentSession
+
+So, for example, to access the `old` key for the session data for the
+sample key above:
+
+ request*PersistentSession/zope.app.actionplan/old
+
+In this example, the data for an aplication key was a mapping object.
+The semantics of a session data for a particular application key are
+determined by the session data type interface.
+`IPersistentSessionData` defines the application data to be a mapping
+object by default. Other data interfaces could specify different
+bahavior.
Modified: Zope3/branches/stub-session/src/zope/app/session/__init__.py
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/__init__.py 2004-07-05 14:48:15 UTC (rev 26100)
+++ Zope3/branches/stub-session/src/zope/app/session/__init__.py 2004-07-05 21:25:38 UTC (rev 26101)
@@ -16,244 +16,4 @@
$Id$
"""
-import sha, time, string, random, hmac, warnings, thread
-from UserDict import IterableUserDict
-from heapq import heapify, heappop
-from persistent import Persistent
-from zope.server.http.http_date import build_http_date
-from zope.interface import implements
-from zope.component import ComponentLookupError
-from zope.app.zapi import getUtility
-from BTrees.OOBTree import OOBTree
-from zope.app.utility.interfaces import ILocalUtility
-from zope.app.annotation.interfaces import IAttributeAnnotatable
-
-from interfaces import \
- IBrowserIdManager, IBrowserId, ICookieBrowserIdManager, \
- ISessionDataContainer, ISession, ISessionProductData, ISessionData
-
-import ZODB
-import ZODB.MappingStorage
-
-cookieSafeTrans = string.maketrans("+/", "-.")
-
-def digestEncode(s):
- """Encode SHA digest for cookie."""
- return s.encode("base64")[:-2].translate(cookieSafeTrans)
-
-
-class BrowserId(str):
- """See zope.app.interfaces.utilities.session.IBrowserId"""
- implements(IBrowserId)
-
- def __new__(cls, request):
- return str.__new__(
- cls, getUtility(IBrowserIdManager).getBrowserId(request)
- )
-
-
-class CookieBrowserIdManager(Persistent):
- """Session service implemented using cookies."""
-
- implements(IBrowserIdManager, ICookieBrowserIdManager,
- ILocalUtility, IAttributeAnnotatable,
- )
-
- __parent__ = __name__ = None
-
- def __init__(self):
- self.namespace = "zope3_cs_%x" % (int(time.time()) - 1000000000)
- self.secret = "%.20f" % random.random()
- self.cookieLifetime = None
-
- def generateUniqueId(self):
- """Generate a new, random, unique id."""
- data = "%.20f%.20f%.20f" % (random.random(), time.time(), time.clock())
- digest = sha.sha(data).digest()
- s = digestEncode(digest)
- # we store a HMAC of the random value together with it, which makes
- # our session ids unforgeable.
- mac = hmac.new(s, self.secret, digestmod=sha).digest()
- return s + digestEncode(mac)
-
- def getRequestId(self, request):
- """Return the browser id encoded in request as a string,
- or None if it's non-existent."""
- # If there is an id set on the response, use that but don't trust it.
- # We need to check the response in case there has already been a new
- # session created during the course of this request.
- response_cookie = request.response.getCookie(self.namespace)
- if response_cookie:
- sid = response_cookie['value']
- else:
- sid = request.cookies.get(self.namespace)
- if sid is None or len(sid) != 54:
- return None
- s, mac = sid[:27], sid[27:]
- if (digestEncode(hmac.new(s, self.secret, digestmod=sha).digest())
- != mac):
- return None
- else:
- return sid
-
- def setRequestId(self, request, id):
- """Set cookie with id on request."""
- # XXX Currently, the path is the ApplicationURL. This is reasonable,
- # and will be adequate for most purposes.
- # A better path to use would be that of the folder that contains
- # the service-manager this service is registered within. However,
- # that would be expensive to look up on each request, and would
- # have to be altered to take virtual hosting into account.
- # Seeing as this service instance has a unique namespace for its
- # cookie, using ApplicationURL shouldn't be a problem.
-
- if self.cookieLifetime is not None:
- if self.cookieLifetime:
- expires = build_http_date(time.time() + self.cookieLifetime)
- else:
- expires = 'Tue, 19 Jan 2038 00:00:00 GMT'
- request.response.setCookie(
- self.namespace, id, expires=expires,
- path=request.getApplicationURL(path_only=True)
- )
- else:
- request.response.setCookie(
- self.namespace, id,
- path=request.getApplicationURL(path_only=True)
- )
-
- def getBrowserId(self, request):
- """See zope.app.session.interfaces.IBrowserIdManager"""
- sid = self.getRequestId(request)
- if sid is None:
- sid = self.generateUniqueId()
- self.setRequestId(request, sid)
- return sid
-
-
-class PersistentSessionDataContainer(Persistent, IterableUserDict):
- ''' A SessionDataContainer that stores data in the ZODB '''
- __parent__ = __name__ = None
-
- implements(ISessionDataContainer, ILocalUtility, IAttributeAnnotatable)
-
- def __init__(self):
- self.data = OOBTree()
- self.sweepInterval = 5*60
-
- def __getitem__(self, product_id):
- rv = IterableUserDict.__getitem__(self, product_id)
- now = time.time()
- # Only update lastAccessTime once every few minutes, rather than
- # every hit, to avoid ZODB bloat and conflicts
- if rv.lastAccessTime + self.sweepInterval < now:
- rv.lastAccessTime = int(now)
- # XXX: When scheduler exists, this method should just schedule
- # a sweep later since we are currently busy handling a request
- # and may end up doing simultaneous sweeps
- self.sweep()
- return rv
-
- def __setitem__(self, product_id, session_data):
- session_data.lastAccessTime = int(time.time())
- return IterableUserDict.__setitem__(self, product_id, session_data)
-
- def sweep(self):
- ''' Clean out stale data '''
- expire_time = time.time() - self.sweepInterval
- heap = [(v.lastAccessTime, k) for k,v in self.data.items()]
- heapify(heap)
- while heap:
- lastAccessTime, key = heappop(heap)
- if lastAccessTime < expire_time:
- del self.data[key]
- else:
- return
-
-
-class RAMSessionDataContainer(PersistentSessionDataContainer):
- ''' A SessionDataContainer that stores data in RAM. Currently session
- data is not shared between Zope clients, so server affinity will
- need to be maintained to use this in a ZEO cluster.
- '''
- def __init__(self):
- self.sweepInterval = 5*60
- self.key = sha.new(str(time.time() + random.random())).hexdigest()
-
- _ram_storage = ZODB.MappingStorage.MappingStorage()
- _ram_db = ZODB.DB(_ram_storage)
- _conns = {}
-
- def _getData(self):
-
- # Open a connection to _ram_storage per thread
- tid = thread.get_ident()
- if not self._conns.has_key(tid):
- self._conns[tid] = self._ram_db.open()
-
- root = self._conns[tid].root()
- if not root.has_key(self.key):
- root[self.key] = OOBTree()
- return root[self.key]
-
- data = property(_getData, None)
-
- def sweep(self):
- super(RAMSessionDataContainer, self).sweep()
- self._ram_db.pack(time.time())
-
-
-class Session:
- """See zope.app.session.interfaces.ISession"""
- implements(ISession)
- __slots__ = ('browser_id',)
- def __init__(self, request):
- self.browser_id = str(IBrowserId(request))
-
- def __getitem__(self, product_id):
- """See zope.app.session.interfaces.ISession"""
-
- # First locate the ISessionDataContainer by looking up
- # the named Utility, and falling back to the unnamed one.
- try:
- sdc = getUtility(ISessionDataContainer, product_id)
- except ComponentLookupError:
- # XXX: Do we want this?
- warnings.warn(
- 'Unable to find ISessionDataContainer named %s. '
- 'Using default' % repr(product_id),
- RuntimeWarning
- )
- sdc = getUtility(ISessionDataContainer)
-
- # The ISessionDataContainer contains two levels:
- # ISessionDataContainer[product_id] == ISessionProductData
- # ISessionDataContainer[product_id][browser_id] == ISessionData
- try:
- spd = sdc[product_id]
- except KeyError:
- sdc[product_id] = SessionProductData()
- spd = sdc[product_id]
-
- try:
- return spd[self.browser_id]
- except KeyError:
- spd[self.browser_id] = SessionData()
- return spd[self.browser_id]
-
-
-class SessionProductData(Persistent, IterableUserDict):
- """See zope.app.session.interfaces.ISessionProductData"""
- implements(ISessionProductData)
- lastAccessTime = 0
- def __init__(self):
- self.data = OOBTree()
-
-
-class SessionData(Persistent, IterableUserDict):
- """See zope.app.session.interfaces.ISessionData"""
- implements(ISessionData)
- def __init__(self):
- self.data = OOBTree()
-
Modified: Zope3/branches/stub-session/src/zope/app/session/bootstrap.py
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/bootstrap.py 2004-07-05 14:48:15 UTC (rev 26100)
+++ Zope3/branches/stub-session/src/zope/app/session/bootstrap.py 2004-07-05 21:25:38 UTC (rev 26101)
@@ -24,19 +24,19 @@
from zope.app.session.interfaces import \
- IBrowserIdManager, ISessionDataContainer
-from zope.app.session import \
- CookieBrowserIdManager, PersistentSessionDataContainer
+ IClientIdManager, ISessionDataContainer
+from zope.app.session.http import CookieClientIdManager
+from zope.app.session.session import PersistentSessionDataContainer
class BootstrapInstance(BootstrapSubscriberBase):
def doSetup(self):
self.ensureUtility(
- IBrowserIdManager, 'CookieBrowserIdManager',
- CookieBrowserIdManager,
+ IClientIdManager, 'CookieClientIdManager',
+ CookieClientIdManager,
)
self.ensureUtility(
- ISessionDataContainer, 'PersistentSessionData',
+ ISessionDataContainer, 'PersistentSessionDataContainer',
PersistentSessionDataContainer,
)
Modified: Zope3/branches/stub-session/src/zope/app/session/browser.zcml
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/browser.zcml 2004-07-05 14:48:15 UTC (rev 26100)
+++ Zope3/branches/stub-session/src/zope/app/session/browser.zcml 2004-07-05 21:25:38 UTC (rev 26101)
@@ -6,49 +6,39 @@
<!-- Cookie Browser Id Manager -->
<addMenuItem
- title="Cookie Browser Id Manager"
- description="Uses a cookie to uniquely identify a browser, allowing
+ title="Cookie Client Id Manager"
+ description="Uses a cookie to uniquely identify a client, allowing
state to be maintained between requests"
- class=".CookieBrowserIdManager"
- permission="zope.ManageContent" />
+ class=".http.CookieClientIdManager"
+ permission="zope.ManageServices" />
- <!-- XXX: We want an add form, but namespace needs to default to a unique
- cookie name
- <addform
- schema=".interfaces.ICookieBrowserIdManager"
- label="Add a Cookie Browser ID Manager"
- content_factory=".CookieBrowserIdManager"
- name="zope.app.interfaces.utilities.session"
- permission="zope.ManageContent" />
- -->
-
<editform
- schema=".interfaces.ICookieBrowserIdManager"
- label="Cookie Browser ID Manager Properties"
+ schema=".http.ICookieClientIdManager"
+ label="Cookie Client Id Manager Properties"
name="edit.html" menu="zmi_views" title="Edit"
- permission="zope.ManageContent" />
+ permission="zope.ManageServices" />
<!-- PersistentSessionDataContainer -->
<addMenuItem
title="Persistent Session Data Container"
description="Stores session data persistently in the ZODB"
- class=".PersistentSessionDataContainer"
- permission="zope.ManageContent" />
+ class=".session.PersistentSessionDataContainer"
+ permission="zope.ManageServices" />
<!-- RAMSessionDataContainer -->
<addMenuItem
title="RAM Session Data Container"
description="Stores session data in RAM"
- class=".RAMSessionDataContainer"
- permission="zope.ManageContent" />
+ class=".session.RAMSessionDataContainer"
+ permission="zope.ManageServices" />
<!-- ISessionDataContainer -->
<editform
schema=".interfaces.ISessionDataContainer"
label="Session Data Container Properties"
name="edit.html" menu="zmi_views" title="Edit"
- permission="zope.ManageContent" />
+ permission="zope.ManageServices" />
</configure>
Copied: Zope3/branches/stub-session/src/zope/app/session/browserid.py (from rev 26090, Zope3/branches/stub-session/src/zope/app/session/__init__.py)
Modified: Zope3/branches/stub-session/src/zope/app/session/configure.zcml
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/configure.zcml 2004-07-05 14:48:15 UTC (rev 26100)
+++ Zope3/branches/stub-session/src/zope/app/session/configure.zcml 2004-07-05 21:25:38 UTC (rev 26101)
@@ -3,56 +3,56 @@
xmlns:browser="http://namespaces.zope.org/browser">
<adapter
- factory=".BrowserId"
- provides=".interfaces.IBrowserId"
+ factory=".session.ClientId"
+ provides=".interfaces.IClientId"
for="zope.publisher.interfaces.IRequest"
permission="zope.Public"
/>
<adapter
- factory=".Session"
+ factory=".session.Session"
provides=".interfaces.ISession"
for="zope.publisher.interfaces.IRequest"
permission="zope.Public"
/>
- <content class=".CookieBrowserIdManager">
+ <content class=".session.Session">
+ <allow interface=".interfaces.ISession" />
+ </content>
+
+ <content class=".http.CookieClientIdManager">
<require
- interface=".interfaces.ICookieBrowserIdManager"
+ interface=".http.ICookieClientIdManager"
permission="zope.Public" />
<require
- set_schema=".interfaces.ICookieBrowserIdManager"
- permission="zope.ManageContent" />
+ set_schema=".http.ICookieClientIdManager"
+ permission="zope.ManageServices" />
</content>
- <content class=".PersistentSessionDataContainer">
- <implements
- interface=".interfaces.ISessionDataContainer"/>
+ <content class=".session.PersistentSessionDataContainer">
<require
interface=".interfaces.ISessionDataContainer"
permission="zope.Public" />
<require
set_schema=".interfaces.ISessionDataContainer"
- permission="zope.ManageContent" />
+ permission="zope.ManageServices" />
</content>
- <content class=".RAMSessionDataContainer">
- <implements
- interface=".interfaces.ISessionDataContainer"/>
+ <content class=".session.RAMSessionDataContainer">
<require
interface=".interfaces.ISessionDataContainer"
permission="zope.Public" />
<require
set_schema=".interfaces.ISessionDataContainer"
- permission="zope.ManageContent" />
+ permission="zope.ManageServices" />
</content>
- <content class=".SessionData">
- <allow interface="zope.interface.common.mapping.IMapping" />
+ <content class=".session.SessionData">
+ <allow interface=".interfaces.ISessionData" />
</content>
- <content class=".Session">
- <allow interface=".interfaces.ISession" />
+ <content class=".session.SessionPkgData">
+ <allow interface=".interfaces.ISessionPkgData" />
</content>
<subscriber
Copied: Zope3/branches/stub-session/src/zope/app/session/http.py (from rev 26089, Zope3/branches/jim-session/src/zope/app/session/http.py)
===================================================================
--- Zope3/branches/jim-session/src/zope/app/session/http.py 2004-07-03 00:55:25 UTC (rev 26089)
+++ Zope3/branches/stub-session/src/zope/app/session/http.py 2004-07-05 21:25:38 UTC (rev 26101)
@@ -0,0 +1,274 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Session implementation using cookies
+
+$Id$
+"""
+
+from persistent import Persistent
+from zope.app.annotation.interfaces import IAttributeAnnotatable
+from zope.app.i18n import ZopeMessageIDFactory as _
+from zope.app import zapi
+from zope.app.session.interfaces import IClientIdManager
+from zope.app.utility.interfaces import ILocalUtility
+from zope import schema
+from zope.interface import implements
+from zope.server.http.http_date import build_http_date
+import hmac
+import random
+import re
+import sha
+import string
+import time
+
+cookieSafeTrans = string.maketrans("+/", "-.")
+
+def digestEncode(s):
+ """Encode SHA digest for cookie."""
+ return s.encode("base64")[:-2].translate(cookieSafeTrans)
+
+class ICookieClientIdManager(IClientIdManager):
+ """Manages sessions using a cookie"""
+
+ namespace = schema.TextLine(
+ title=_('Cookie Name'),
+ description=_(
+ "Name of cookie used to maintain state. "
+ "Must be unique to the site domain name, and only contain "
+ "ASCII letters, digits and '_'"
+ ),
+ required=True,
+ min_length=1,
+ max_length=30,
+ constraint=re.compile("^[\d\w_]+$").search,
+ )
+
+ cookieLifetime = schema.Int(
+ title=_('Cookie Lifetime'),
+ description=_(
+ "Number of seconds until the browser expires the cookie. "
+ "Leave blank expire the cookie when the browser is quit. "
+ "Set to 0 to never expire. "
+ ),
+ min=0,
+ required=False,
+ default=None,
+ missing_value=None,
+ )
+
+class CookieClientIdManager(Persistent):
+ """Session service implemented using cookies."""
+
+ implements(IClientIdManager, ICookieClientIdManager,
+ ILocalUtility, IAttributeAnnotatable,
+ )
+
+ __parent__ = __name__ = None
+
+ def __init__(self):
+ self.namespace = "zope3_cs_%x" % (int(time.time()) - 1000000000)
+ self.secret = "%.20f" % random.random()
+ self.cookieLifetime = None
+
+ def getClientId(self, request):
+ """Get the client id
+
+ This creates one if necessary:
+
+ >>> from zope.publisher.http import HTTPRequest
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bim = CookieClientIdManager()
+ >>> id = bim.getClientId(request)
+ >>> id == bim.getClientId(request)
+ True
+
+ The id is retained accross requests:
+
+ >>> request2 = HTTPRequest(None, None, {}, None)
+ >>> request2._cookies = dict(
+ ... [(name, cookie['value'])
+ ... for (name, cookie) in request.response._cookies.items()
+ ... ])
+ >>> id == bim.getClientId(request2)
+ True
+ >>> bool(id)
+ True
+
+ Note that the return value of this function is a string, not
+ an IClientId. This is because this method is used to implement
+ the IClientId Adapter.
+
+ >>> type(id) == type('')
+ True
+
+ """
+ sid = self.getRequestId(request)
+ if sid is None:
+ sid = self.generateUniqueId()
+ self.setRequestId(request, sid)
+ return sid
+
+ def generateUniqueId(self):
+ """Generate a new, random, unique id.
+
+ >>> bim = CookieClientIdManager()
+ >>> id1 = bim.generateUniqueId()
+ >>> id2 = bim.generateUniqueId()
+ >>> id1 != id2
+ True
+
+ """
+ data = "%.20f%.20f%.20f" % (random.random(), time.time(), time.clock())
+ digest = sha.sha(data).digest()
+ s = digestEncode(digest)
+ # we store a HMAC of the random value together with it, which makes
+ # our session ids unforgeable.
+ mac = hmac.new(s, self.secret, digestmod=sha).digest()
+ return s + digestEncode(mac)
+
+ def getRequestId(self, request):
+ """Return the browser id encoded in request as a string
+
+ Return None if an id is not set.
+
+ For example:
+
+ >>> from zope.publisher.http import HTTPRequest
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bim = CookieClientIdManager()
+
+ Because no cookie has been set, we get no id:
+
+ >>> bim.getRequestId(request) is None
+ True
+ >>> id1 = bim.generateUniqueId()
+
+ We can set an id:
+
+ >>> bim.setRequestId(request, id1)
+
+ And get it back:
+
+ >>> bim.getRequestId(request) == id1
+ True
+
+ When we set the request id, we also set a response cookie. We
+ can simulate getting this cookie back in a subsequent request:
+
+ >>> request2 = HTTPRequest(None, None, {}, None)
+ >>> request2._cookies = dict(
+ ... [(name, cookie['value'])
+ ... for (name, cookie) in request.response._cookies.items()
+ ... ])
+
+ And we get the same id back from the new request:
+
+ >>> bim.getRequestId(request) == bim.getRequestId(request2)
+ True
+
+ """
+
+ # If there is an id set on the response, use that but don't trust it.
+ # We need to check the response in case there has already been a new
+ # session created during the course of this request.
+ response_cookie = request.response.getCookie(self.namespace)
+ if response_cookie:
+ sid = response_cookie['value']
+ else:
+ sid = request.cookies.get(self.namespace)
+ if sid is None or len(sid) != 54:
+ return None
+ s, mac = sid[:27], sid[27:]
+ if (digestEncode(hmac.new(s, self.secret, digestmod=sha).digest())
+ != mac):
+ return None
+ else:
+ return sid
+
+ def setRequestId(self, request, id):
+ """Set cookie with id on request.
+
+ This sets the response cookie:
+
+ See the examples in getRequestId.
+
+ Note that the id is checkec for validity. Setting an
+ invalid value is silently ignored:
+
+ >>> from zope.publisher.http import HTTPRequest
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bim = CookieClientIdManager()
+ >>> bim.getRequestId(request)
+ >>> bim.setRequestId(request, 'invalid id')
+ >>> bim.getRequestId(request)
+
+ For now, the cookie path is the application URL:
+
+ >>> cookie = request.response.getCookie(bim.namespace)
+ >>> cookie['path'] == request.getApplicationURL(path_only=True)
+ True
+
+ In the future, it should be the site containing the
+ CookieClientIdManager
+
+ By default, session cookies don't expire:
+
+ >>> cookie.has_key('expires')
+ False
+
+ Expiry time of 0 means never (well - close enough)
+
+ >>> bim.cookieLifetime = 0
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bid = bim.getClientId(request)
+ >>> cookie = request.response.getCookie(bim.namespace)
+ >>> cookie['expires']
+ 'Tue, 19 Jan 2038 00:00:00 GMT'
+
+ A non-zero value means to expire after than number of seconds:
+
+ >>> bim.cookieLifetime = 3600
+ >>> request = HTTPRequest(None, None, {}, None)
+ >>> bid = bim.getClientId(request)
+ >>> cookie = request.response.getCookie(bim.namespace)
+ >>> import rfc822
+ >>> expires = time.mktime(rfc822.parsedate(cookie['expires']))
+ >>> expires > time.mktime(time.gmtime()) + 55*60
+ True
+ """
+ # XXX Currently, the path is the ApplicationURL. This is reasonable,
+ # and will be adequate for most purposes.
+ # A better path to use would be that of the folder that contains
+ # the service-manager this service is registered within. However,
+ # that would be expensive to look up on each request, and would
+ # have to be altered to take virtual hosting into account.
+ # Seeing as this service instance has a unique namespace for its
+ # cookie, using ApplicationURL shouldn't be a problem.
+
+ if self.cookieLifetime is not None:
+ if self.cookieLifetime:
+ expires = build_http_date(time.time() + self.cookieLifetime)
+ else:
+ expires = 'Tue, 19 Jan 2038 00:00:00 GMT'
+ request.response.setCookie(
+ self.namespace, id, expires=expires,
+ path=request.getApplicationURL(path_only=True)
+ )
+ else:
+ request.response.setCookie(
+ self.namespace, id,
+ path=request.getApplicationURL(path_only=True)
+ )
+
Modified: Zope3/branches/stub-session/src/zope/app/session/interfaces.py
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/interfaces.py 2004-07-05 14:48:15 UTC (rev 26100)
+++ Zope3/branches/stub-session/src/zope/app/session/interfaces.py 2004-07-05 21:25:38 UTC (rev 26101)
@@ -15,7 +15,6 @@
$Id$
"""
-import re
from zope.interface import Interface
from zope.interface.common.mapping import IMapping, IReadMapping, IWriteMapping
from zope import schema
@@ -23,11 +22,11 @@
from zope.app.i18n import ZopeMessageIDFactory as _
-class IBrowserIdManager(Interface):
- """Manages sessions - fake state over multiple browser requests."""
+class IClientIdManager(Interface):
+ """Manages sessions - fake state over multiple client requests."""
- def getBrowserId(request):
- """Return the browser id for the given request as a string.
+ def getClientId(request):
+ """Return the client id for the given request as a string.
If the request doesn't have an attached sessionId a new one will be
generated.
@@ -46,44 +45,14 @@
"""
-class ICookieBrowserIdManager(IBrowserIdManager):
- """Manages sessions using a cookie"""
+class IClientId(Interface):
+ """A unique id representing a session"""
- namespace = schema.TextLine(
- title=_('Cookie Name'),
- description=_(
- "Name of cookie used to maintain state. "
- "Must be unique to the site domain name, and only contain "
- "ASCII letters, digits and '_'"
- ),
- required=True,
- min_length=1,
- max_length=30,
- constraint=re.compile("^[\d\w_]+$").search,
- )
-
- cookieLifetime = schema.Int(
- title=_('Cookie Lifetime'),
- description=_(
- "Number of seconds until the browser expires the cookie. "
- "Leave blank expire the cookie when the browser is quit. "
- "Set to 0 to never expire. "
- ),
- min=0,
- required=False,
- default=None,
- missing_value=None,
- )
-
-
-class IBrowserId(Interface):
- """A unique ID representing a session"""
-
def __str__():
"""As a unique ASCII string"""
-class ISessionDataContainer(IMapping):
+class ISessionDataContainer(IReadMapping, IWriteMapping):
"""Stores data objects for sessions.
The object implementing this interface is responsible for expiring data as
@@ -93,34 +62,38 @@
session_data_container[product_id][browser_id][key] = value
- Attempting to access a key that does not exist will raise a KeyError.
+ Note that this interface does not support the full mapping interface -
+ the keys need to remain secret so we can't give access to keys(),
+ values() etc.
"""
-
timeout = schema.Int(
title=_(u"Timeout"),
description=_(
"Number of seconds before data becomes stale and may "
- "be removed"),
+ "be removed. A value of '0' means no expiration."),
default=3600,
required=True,
- min=1,
+ min=0,
)
- sweepInterval = schema.Int(
- title=_(u"Purge Interval"),
+ resolution = schema.Int(
+ title=_("Timeout resolution (in seconds)"),
description=_(
- "How often stale data is purged in seconds. "
- "Higer values improve performance."
+ "Defines what the 'resolution' of item timeout is. "
+ "Setting this higher allows the transience machinery to "
+ "do fewer 'writes' at the expense of causing items to time "
+ "out later than the 'Data object timeout value' by a factor "
+ "of (at most) this many seconds."
),
default=5*60,
required=True,
- min=1,
+ min=0,
)
def __getitem__(self, product_id):
- """Return an ISessionProductData"""
+ """Return an ISessionPkgData"""
def __setitem__(self, product_id, value):
- """Store an ISessionProductData"""
+ """Store an ISessionPkgData"""
class ISession(Interface):
@@ -129,6 +102,10 @@
>>> session = ISession(request)[product_id]
>>> session['color'] = 'red'
+ True
+
+ >>> ISessionData.providedBy(session)
+ True
"""
def __getitem__(product_id):
@@ -136,9 +113,9 @@
and return that product id's ISessionData"""
-class ISessionProductData(IReadMapping, IWriteMapping):
+class ISessionData(IReadMapping, IMapping):
"""Storage for a particular product id's session data, containing
- 0 or more ISessionData instances"""
+ 0 or more ISessionPkgData instances"""
lastAccessTime = schema.Int(
title=_("Last Access Time"),
@@ -150,13 +127,20 @@
required=True,
)
+ # Note that only IReadMapping and IWriteMaping are implemented.
+ # We cannot give access to the keys, as they need to remain secret.
+
def __getitem__(self, browser_id):
"""Return an ISessionData"""
def __setitem__(self, browser_id, session_data):
"""Store an ISessionData"""
-class ISessionData(IMapping):
- """Storage for a particular product id and browser id's session data"""
+class ISessionPkgData(IMapping):
+ """Storage for a particular product id and browser id's session data
+ Data is stored persistently and transactionally. Data stored must
+ be persistent or pickable.
+ """
+
Copied: Zope3/branches/stub-session/src/zope/app/session/persist.py (from rev 26090, Zope3/branches/stub-session/src/zope/app/session/__init__.py)
Copied: Zope3/branches/stub-session/src/zope/app/session/session.py (from rev 26099, Zope3/branches/stub-session/src/zope/app/session/__init__.py)
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/__init__.py 2004-07-04 00:26:23 UTC (rev 26099)
+++ Zope3/branches/stub-session/src/zope/app/session/session.py 2004-07-05 21:25:38 UTC (rev 26101)
@@ -0,0 +1,352 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""
+Session implementation
+
+$Id$
+"""
+import sha, time, string, random, hmac, warnings, thread, zope.interface
+from UserDict import IterableUserDict
+from heapq import heapify, heappop
+
+from zope.app.i18n import ZopeMessageIDFactory as _
+from persistent import Persistent
+from zope import schema
+from zope.interface import implements
+from zope.component import ComponentLookupError
+from zope.app.zapi import getUtility
+from BTrees.OOBTree import OOBTree
+from zope.app.utility.interfaces import ILocalUtility
+from zope.app.annotation.interfaces import IAttributeAnnotatable
+
+from interfaces import \
+ IClientIdManager, IClientId, ISession, ISessionDataContainer, \
+ ISessionPkgData, ISessionData
+
+from http import ICookieClientIdManager
+
+import ZODB
+import ZODB.MappingStorage
+
+cookieSafeTrans = string.maketrans("+/", "-.")
+
+def digestEncode(s):
+ """Encode SHA digest for cookie."""
+ return s.encode("base64")[:-2].translate(cookieSafeTrans)
+
+
+class ClientId(str):
+ """See zope.app.interfaces.utilities.session.IClientId
+
+ >>> import tests
+ >>> request = tests.setUp()
+
+ >>> id1 = ClientId(request)
+ >>> id2 = ClientId(request)
+ >>> id1 == id2
+ True
+
+ >>> tests.tearDown()
+ """
+ implements(IClientId)
+
+ def __new__(cls, request):
+ return str.__new__(
+ cls, getUtility(IClientIdManager).getClientId(request)
+ )
+
+
+class PersistentSessionDataContainer(Persistent, IterableUserDict):
+ """A SessionDataContainer that stores data in the ZODB"""
+ __parent__ = __name__ = None
+
+ implements(ISessionDataContainer, ILocalUtility, IAttributeAnnotatable)
+
+ _v_last_sweep = 0 # Epoch time sweep last run
+
+ def __init__(self):
+ self.data = OOBTree()
+ self.timeout = 1 * 60 * 60
+ self.resolution = 50*60
+
+ def __getitem__(self, pkg_id):
+ """Retrieve an IApplicationSessionData
+
+ >>> sdc = PersistentSessionDataContainer()
+
+ >>> sdc.timeout = 60
+ >>> sdc.resolution = 3
+ >>> sdc['clientid'] = sd = SessionData()
+
+ To ensure stale data is removed, we can wind
+ back the clock using undocumented means...
+
+ >>> sd.lastAccessTime = sd.lastAccessTime - 64
+ >>> sdc._v_last_sweep = sdc._v_last_sweep - 4
+
+ Now the data should be garbage collected
+
+ >>> sdc['clientid']
+ Traceback (most recent call last):
+ [...]
+ KeyError: 'clientid'
+
+ Ensure lastAccessTime on the ISessionData is being updated
+ occasionally. The ISessionDataContainer maintains this whenever
+ the ISessionData is set or retrieved.
+
+ lastAccessTime on the ISessionData is set when it is added
+ to the ISessionDataContainer
+
+ >>> sdc['client_id'] = sd = SessionData()
+ >>> sd.lastAccessTime > 0
+ True
+
+ lastAccessTime is also updated whenever the ISessionData
+ is retrieved through the ISessionDataContainer, at most
+ once every 'resolution' seconds.
+
+ >>> then = sd.lastAccessTime = sd.lastAccessTime - 4
+ >>> now = sdc['client_id'].lastAccessTime
+ >>> now > then
+ True
+ >>> time.sleep(1)
+ >>> now == sdc['client_id'].lastAccessTime
+ True
+
+ Ensure lastAccessTime is not modified and no garbage collection
+ occurs when timeout == 0. We test this by faking a stale
+ ISessionData object.
+
+ >>> sdc.timeout = 0
+ >>> sd.lastAccessTime = sd.lastAccessTime - 5000
+ >>> lastAccessTime = sd.lastAccessTime
+ >>> sdc['client_id'].lastAccessTime == lastAccessTime
+ True
+ """
+ if self.timeout == 0:
+ return IterableUserDict.__getitem__(self, pkg_id)
+
+ now = time.time()
+
+ # XXX: When scheduler exists, sweeping should be done by
+ # a scheduled job since we are currently busy handling a
+ # request and may end up doing simultaneous sweeps
+ if self._v_last_sweep + self.resolution < now:
+ self.sweep()
+ self._v_last_sweep = now
+
+ rv = IterableUserDict.__getitem__(self, pkg_id)
+ # Only update lastAccessTime once every few minutes, rather than
+ # every hit, to avoid ZODB bloat and conflicts
+ if rv.lastAccessTime + self.resolution < now:
+ rv.lastAccessTime = int(now)
+ return rv
+
+ def __setitem__(self, pkg_id, session_data):
+ """Set an ISessionPkgData
+
+ >>> sdc = PersistentSessionDataContainer()
+ >>> sad = SessionData()
+
+ __setitem__ sets the ISessionData's lastAccessTime
+
+ >>> sad.lastAccessTime
+ 0
+ >>> sdc['1'] = sad
+ >>> 0 < sad.lastAccessTime <= time.time()
+ True
+
+ We can retrieve the same object we put in
+
+ >>> sdc['1'] is sad
+ True
+ """
+ session_data.lastAccessTime = int(time.time())
+ return IterableUserDict.__setitem__(self, pkg_id, session_data)
+
+ def sweep(self):
+ """Clean out stale data
+
+ >>> sdc = PersistentSessionDataContainer()
+ >>> sdc['1'] = SessionData()
+ >>> sdc['2'] = SessionData()
+
+ Wind back the clock on one of the ISessionData's
+ so it gets garbage collected
+
+ >>> sdc['2'].lastAccessTime -= sdc.timeout * 2
+
+ Sweep should leave '1' and remove '2'
+
+ >>> sdc.sweep()
+ >>> sd1 = sdc['1']
+ >>> sd2 = sdc['2']
+ Traceback (most recent call last):
+ [...]
+ KeyError: '2'
+ """
+ # We only update the lastAccessTime every 'resolution' seconds.
+ # To compensate for this, we factor in the resolution when
+ # calculating the expiry time to ensure that we never remove
+ # data that has been accessed within timeout seconds.
+ expire_time = time.time() - self.timeout - self.resolution
+ heap = [(v.lastAccessTime, k) for k,v in self.data.items()]
+ heapify(heap)
+ while heap:
+ lastAccessTime, key = heappop(heap)
+ if lastAccessTime < expire_time:
+ del self.data[key]
+ else:
+ return
+
+
+class RAMSessionDataContainer(PersistentSessionDataContainer):
+ """A SessionDataContainer that stores data in RAM. Currently session
+ data is not shared between Zope clients, so server affinity will
+ need to be maintained to use this in a ZEO cluster.
+
+ >>> sdc = RAMSessionDataContainer()
+ >>> sdc['1'] = SessionData()
+ >>> sdc['1'] is sdc['1']
+ True
+ >>> ISessionData.providedBy(sdc['1'])
+ True
+ """
+ def __init__(self):
+ self.resolution = 5*60
+ self.timeout = 1 * 60 * 60
+ # Something unique
+ self.key = '%s.%s.%s' % (time.time(), random.random(), id(self))
+
+ _ram_storage = ZODB.MappingStorage.MappingStorage()
+ _ram_db = ZODB.DB(_ram_storage)
+ _conns = {}
+
+ def _getData(self):
+
+ # Open a connection to _ram_storage per thread
+ tid = thread.get_ident()
+ if not self._conns.has_key(tid):
+ self._conns[tid] = self._ram_db.open()
+
+ root = self._conns[tid].root()
+ if not root.has_key(self.key):
+ root[self.key] = OOBTree()
+ return root[self.key]
+
+ data = property(_getData, None)
+
+ def sweep(self):
+ super(RAMSessionDataContainer, self).sweep()
+ self._ram_db.pack(time.time())
+
+
+class Session:
+ """See zope.app.session.interfaces.ISession"""
+ implements(ISession)
+ __slots__ = ('browser_id',)
+ def __init__(self, request):
+ self.client_id = str(IClientId(request))
+
+ def __getitem__(self, pkg_id):
+ """See zope.app.session.interfaces.ISession
+
+ >>> import tests
+ >>> request = tests.setUp(PersistentSessionDataContainer)
+ >>> request2 = tests.HTTPRequest(None, None, {}, None)
+
+ >>> ISession.providedBy(Session(request))
+ True
+
+ Setup some sessions, each with a distinct namespace
+
+ >>> session1 = Session(request)['products.foo']
+ >>> session2 = Session(request)['products.bar']
+ >>> session3 = Session(request2)['products.bar']
+
+ If we use the same parameters, we should retrieve the
+ same object
+
+ >>> session1 is Session(request)['products.foo']
+ True
+
+ Make sure it returned sane values
+
+ >>> ISessionPkgData.providedBy(session1)
+ True
+
+ Make sure that pkg_ids don't share a namespace.
+
+ >>> session1['color'] = 'red'
+ >>> session2['color'] = 'blue'
+ >>> session3['color'] = 'vomit'
+ >>> session1['color']
+ 'red'
+ >>> session2['color']
+ 'blue'
+ >>> session3['color']
+ 'vomit'
+
+ >>> tests.tearDown()
+ """
+
+ # First locate the ISessionDataContainer by looking up
+ # the named Utility, and falling back to the unnamed one.
+ try:
+ sdc = getUtility(ISessionDataContainer, pkg_id)
+ except ComponentLookupError:
+ sdc = getUtility(ISessionDataContainer)
+
+ # The ISessionDataContainer contains two levels:
+ # ISessionDataContainer[client_id] == ISessionData
+ # ISessionDataContainer[client_id][pkg_id] == ISessionPkgData
+ try:
+ sd = sdc[self.client_id]
+ except KeyError:
+ sd = sdc[self.client_id] = SessionData()
+
+ try:
+ return sd[pkg_id]
+ except KeyError:
+ spd = sd[pkg_id] = SessionPkgData()
+ return spd
+
+
+class SessionData(Persistent, IterableUserDict):
+ """See zope.app.session.interfaces.ISessionData
+
+ >>> session = SessionData()
+ >>> ISessionData.providedBy(session)
+ True
+ >>> session.lastAccessTime
+ 0
+ """
+ implements(ISessionData)
+ lastAccessTime = 0
+ def __init__(self):
+ self.data = OOBTree()
+
+
+class SessionPkgData(Persistent, IterableUserDict):
+ """See zope.app.session.interfaces.ISessionData
+
+ >>> session = SessionPkgData()
+ >>> ISessionPkgData.providedBy(session)
+ True
+ """
+ implements(ISessionPkgData)
+ def __init__(self):
+ self.data = OOBTree()
+
Modified: Zope3/branches/stub-session/src/zope/app/session/session.stx
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/session.stx 2004-07-05 14:48:15 UTC (rev 26100)
+++ Zope3/branches/stub-session/src/zope/app/session/session.stx 2004-07-05 21:25:38 UTC (rev 26101)
@@ -1,55 +1,108 @@
-Session Support
----------------
+Zope3 Session Implementation
+============================
+Overview
+--------
+
Sessions allow us to fake state over a stateless protocol - HTTP. We do this
by having a unique identifier stored across multiple HTTP requests, be it
a cookie or some id mangled into the URL.
-The IBrowserIdManager Utility provides this unique id. It is responsible
-for propagating this id so that future requests from the browser get
-the same id (eg. by setting an HTTP cookie)
+The IClientIdManager Utility provides this unique id. It is responsible
+for propagating this id so that future requests from the client get
+the same id (eg. by setting an HTTP cookie). This utility is used
+when we adapt the request to the unique client id:
-ISessionDataContainer Utilities store session data. The ISessionDataContainer
-is responsible for expiring data.
+ >>> client_id = IClientId(request)
-ISessionDataContainer[product_id] returns ISessionProductData
-ISessionDataContainer[product_id][browser_id] returns ISessionData
+The ISession adapter gives us a mapping that can be used to store
+and retrieve session data. A unique key (the package id) is used
+to avoid namespace clashes:
-ISession(request)[product_id] returns ISessionData
+ >>> pkg_id = 'products.foo'
+ >>> session = ISession(request)[pkg_id]
+ >>> session['color'] = 'red'
-An ISession determines what ISessionDataContainer to use by looking
-up an ISessionDataContainer using the product_id as the name, and
-falling back to the unnamed ISessionDataContainer utility. This allows
-site administrators to select which ISessionDataContainer a particular
-product stores its session data in by registering the utility with
-the relevant name(s).
+ >>> session2 = ISession(request)['products.bar']
+ >>> session2['color'] = 'blue'
-Python example::
+ >>> session['color']
+ 'red'
+ >>> session2['color']
+ 'blue'
- >>> browser_id = IBrowserId(request)
- >>> session_data = ISession(request)['zopeproducts.fooprod']
- >>> session_data['color'] = 'red'
+Data Storage
+------------
+
+The actual data is stored in an ISessionDataContainer utility.
+ISession chooses which ISessionDataContainer should be used by
+looking up as a named utility using the package id. This allows
+the site administrator to configure where the session data is actually
+stored by adding a registration for desired ISessionDataContainer
+with the correct name.
- or for the adventurous....
+ >>> sdc = zapi.getUtility(ISessionDataContainer, pkg_id)
+ >>> sdc[client_id][pkg_id] is session
+ True
+ >>> sdc[client_id][pkg_id]['color']
+ 'red'
- >>> explicit_dc = getUtility(ISessionDataContainer, 'zopeproducts.fooprod')
- >>> session_data = explicit_dc['zopeproducts.fooprod'][str(browser_id)]
- >>> session_data = Session(explicit_dc, browser_id)['zopeproducts.fooprod']
- >>> session_data['color'] = 'red'
+If no ISessionDataContainer utility can be located by name using the
+package id, then the unnamed ISessionDataContainer utility is used as
+a fallback. An unnamed ISessionDataContainer is automatically created
+for you, which may replaced with a different implementation if desired.
+ >>> ISession(request)['unknown'] \
+ ... is zapi.getUtility(ISessionDataContainer)[client_id]['unknown']
+ True
-Page Template example::
+The ISessionDataContainer contains ISessionData objects, and ISessionData
+objects in turn contain ISessionPkgData objects. You should never need
+to know this unless you are writing administrative views for the session
+machinery.
- XXX: Needs update when TALES adapter syntax decided
+ >>> ISessionData.providedBy(sdc[client_id])
+ True
+ >>> ISessionPkgData.providedBy(sdc[client_id][pkg_id])
+ True
- <tal:x condition="exists:session/zopeproducts.fooprod/count">
- <tal:x condition="python:
- session['zopeproducts.fooprod']['count'] += 1" />
- </tal:x>
- <tal:x condition="not:exists:session/zopeprodicts.fooprod/count">
- <tal:x condition="python:
- session['zopeproducts.fooprod']['count'] = 1 />
- </tal:x>
- <span content="session/zopeproducts.fooprod/count">6</span>
+The ISessionDataContainer is responsible for expiring session data.
+The expiry time can be configured by settings its 'timeout' attribute.
+ >>> sdc.timeout = 1200 # 1200 seconds or 20 minutes
+
+
+Restrictions
+------------
+
+Data stored in the session must be persistent or picklable.
+
+ >>> session['oops'] = open(__file__)
+ >>> import transaction
+ >>> transaction.commit()
+ Traceback (most recent call last):
+ [...]
+ TypeError: can't pickle file objects
+
+
+Page Templates
+--------------
+
+ Session data may be accessed in page template documents using the
+ TALES adaptor syntax::
+
+ <span tal:content="request*Session/products.foo/color | default">
+ green
+ </span>
+
+ <div tal:define="session request*Session/products.foo">
+ <tal:x condition="not:exists:session/count">
+ <tal:x condition="python: session['count'] = 1" />
+ </tal:x>
+ <tal:x condition="exists:session/count">
+ <tal:x condition="python: session['count'] += 1" />
+ </tal:x>
+ <span content="session/count">6</span>
+ </div>
+
Modified: Zope3/branches/stub-session/src/zope/app/session/tests.py
===================================================================
--- Zope3/branches/stub-session/src/zope/app/session/tests.py 2004-07-05 14:48:15 UTC (rev 26100)
+++ Zope3/branches/stub-session/src/zope/app/session/tests.py 2004-07-05 21:25:38 UTC (rev 26101)
@@ -15,310 +15,40 @@
'''
$Id$
'''
-import unittest, doctest, time, rfc822
+import unittest, doctest, os, os.path, sys
from zope.app import zapi
-from zope.app.tests import ztapi
-from zope.app.tests import setup
-import zope.interface
-from zope.app.utility.interfaces import ILocalUtility
-from zope.app.utility import LocalUtilityService
-from zope.app.servicenames import Utilities
-from zope.app.annotation.interfaces import IAttributeAnnotatable
+from zope.app.tests import ztapi, placelesssetup
from zope.app.session.interfaces import \
- IBrowserId, IBrowserIdManager, \
- ISession, ISessionDataContainer, ISessionData, ISessionProductData
+ IClientId, IClientIdManager, ISession, ISessionDataContainer, \
+ ISessionPkgData, ISessionData
-from zope.app.session import \
- BrowserId, CookieBrowserIdManager, \
- PersistentSessionDataContainer, RAMSessionDataContainer, \
- Session, SessionData, SessionProductData
+from zope.app.session.session import \
+ ClientId, Session, \
+ PersistentSessionDataContainer, RAMSessionDataContainer
+from zope.app.session.http import CookieClientIdManager
+
from zope.publisher.interfaces import IRequest
-from zope.publisher.interfaces.http import IHTTPRequest
from zope.publisher.http import HTTPRequest
-def setUp(session_data_container_class):
+from zope.pagetemplate.pagetemplate import PageTemplate
- # Placeful setup
- root = setup.placefulSetUp(site=True)
- setup.createStandardServices(root)
- sm = setup.createServiceManager(root, True)
- setup.addService(sm, Utilities, LocalUtilityService())
-
- # Add a CookieBrowserIdManager Utility
- setup.addUtility(sm, '', IBrowserIdManager, CookieBrowserIdManager())
-
- # Add an ISessionDataContainer, registered under a number of names
+def setUp(session_data_container_class=PersistentSessionDataContainer):
+ placelesssetup.setUp()
+ ztapi.provideAdapter(IRequest, IClientId, ClientId)
+ ztapi.provideAdapter(IRequest, ISession, Session)
+ ztapi.provideUtility(IClientIdManager, CookieClientIdManager())
sdc = session_data_container_class()
for product_id in ('', 'products.foo', 'products.bar', 'products.baz'):
- setup.addUtility(sm, product_id, ISessionDataContainer, sdc)
-
- # Register our adapters
- ztapi.provideAdapter(IRequest, IBrowserId, BrowserId)
- ztapi.provideAdapter(IRequest, ISession, Session)
-
- # Return a request
+ ztapi.provideUtility(ISessionDataContainer, sdc, product_id)
request = HTTPRequest(None, None, {}, None)
return request
def tearDown():
- setup.placefulTearDown()
+ placelesssetup.tearDown()
-def test_CookieBrowserIdManager():
- """
- CookieBrowserIdManager.generateUniqueId should generate a unique
- IBrowserId each time it is called
-
- >>> bim = CookieBrowserIdManager()
- >>> id1 = bim.generateUniqueId()
- >>> id2 = bim.generateUniqueId()
- >>> id1 != id2
- True
-
- CookieBrowserIdManager.getRequestId pulls the browser id from an
- IHTTPRequest, or returns None if there isn't one stored in it.
- Because cookies cannnot be trusted, we confirm that they are not forged,
- returning None if we have a corrupt or forged browser id.
-
- >>> request = HTTPRequest(None, None, {}, None)
- >>> bim.getRequestId(request) is None
- True
- >>> bim.setRequestId(request, id1)
- >>> bim.getRequestId(request) == id1
- True
- >>> bim.setRequestId(request, 'invalid_id')
- >>> bim.getRequestId(request) is None
- True
-
- Make sure that the same browser id is extracted from a cookie in
- request (sent from the browser) and a cookie in request.response
- (set during this transaction)
-
- >>> request2 = HTTPRequest(None, None, {}, None)
- >>> request2._cookies = request.response._cookies
- >>> bim.getRequestId(request) == bim.getRequestId(request2)
- True
-
- CookieBrowserIdManager.getBrowserId pulls the browser id from an
- IHTTPRequest, or generates a new one and returns it after storing
- it in the request.
-
- >>> id3 = bim.getBrowserId(request)
- >>> id4 = bim.getBrowserId(request)
- >>> id3 == id4
- True
- >>> id3 == id4
- True
- >>> bool(id3)
- True
-
- Confirm the path of the cookie is correct. The value being tested
- for here will eventually change - it should be the URL to the
- site containing the CookieBrowserIdManager
-
- >>> cookie = request.response.getCookie(bim.namespace)
- >>> cookie['path'] == request.getApplicationURL(path_only=True)
- True
-
- Confirm the expiry time of the cookie is correct.
- Default is no expires.
-
- >>> cookie.has_key('expires')
- False
-
- Expiry time of 0 means never (well - close enough)
-
- >>> bim.cookieLifetime = 0
- >>> request = HTTPRequest(None, None, {}, None)
- >>> bid = bim.getBrowserId(request)
- >>> cookie = request.response.getCookie(bim.namespace)
- >>> cookie['expires']
- 'Tue, 19 Jan 2038 00:00:00 GMT'
-
- >>> bim.cookieLifetime = 3600
- >>> request = HTTPRequest(None, None, {}, None)
- >>> bid = bim.getBrowserId(request)
- >>> cookie = request.response.getCookie(bim.namespace)
- >>> expires = time.mktime(rfc822.parsedate(cookie['expires']))
- >>> expires > time.mktime(time.gmtime()) + 55*60
- True
- """
-
-
-def test_BrowserId():
- """
- >>> request = setUp(PersistentSessionDataContainer)
-
- >>> id1 = BrowserId(request)
- >>> id2 = BrowserId(request)
- >>> id1 == id2
- True
-
- >>> tearDown()
- """
-
-
-def test_PersistentSessionDataContainer():
- """
- Ensure mapping interface is working as expected
-
- >>> sdc = PersistentSessionDataContainer()
- >>> sdc['a']
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 19, in __getitem__
- def __getitem__(self, key): return self.data[key]
- KeyError: 'a'
- >>> sdc['a'] = SessionData()
- >>> pdict = SessionData()
- >>> sdc['a'] = pdict
- >>> id(pdict) == id(sdc['a'])
- True
- >>> del sdc['a']
- >>> sdc['a']
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 19, in __getitem__
- def __getitem__(self, key): return self.data[key]
- KeyError: 'a'
- >>> del sdc['a']
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- File "/usr/python-2.3.3/lib/python2.3/UserDict.py", line 21, in __delitem__
- def __delitem__(self, key): del self.data[key]
- KeyError: 'a'
-
- Make sure stale data is removed
-
- >>> sdc.sweepInterval = 60
- >>> sdc[1], sdc[2] = sd1, sd2 = SessionData(), SessionData()
- >>> ignore = sdc[1], sdc[2]
- >>> sd1.lastAccessTime = sd1.lastAccessTime - 62
- >>> sd2.lastAccessTime = sd2.lastAccessTime - 62
- >>> ignore = sdc[1]
- >>> sdc.get(2, 'stale')
- 'stale'
-
- Ensure lastAccessTime on the ISessionData is being updated
- occasionally. The ISessionDataContainer maintains this whenever
- the ISessionData is retrieved.
-
- >>> sd = SessionData()
- >>> sdc['product_id'] = sd
- >>> sd.lastAccessTime > 0
- True
- >>> last1 = sd.lastAccessTime - 62
- >>> sd.lastAccessTime = last1 # Wind back the clock
- >>> last1 < sdc['product_id'].lastAccessTime
- True
- """
-
-
-def test_RAMSessionDataContainer(self):
- pass
-test_RAMSessionDataContainer.__doc__ = \
- test_PersistentSessionDataContainer.__doc__.replace(
- 'PersistentSessionDataContainer', 'RAMSessionDataContainer'
- )
-
-
-def test_SessionProductData():
- """
- >>> session = SessionProductData()
- >>> ISessionProductData.providedBy(session)
- True
- """
-
-
-def test_SessionData():
- """
- >>> session = SessionData()
-
- Is the interface defined?
-
- >>> ISessionData.providedBy(session)
- True
-
- Make sure it actually works
-
- >>> session['color']
- Traceback (most recent call last):
- File "<stdin>", line 1, in ?
- File "zope/app/utilities/session.py", line 157, in __getitem__
- return self._data[key]
- KeyError: 'color'
- >>> session['color'] = 'red'
- >>> session['color']
- 'red'
-
- Test the rest of the dictionary interface...
-
- >>> 'foo' in session
- False
- >>> 'color' in session
- True
- >>> session.get('size', 'missing')
- 'missing'
- >>> session.get('color', 'missing')
- 'red'
- >>> list(session.keys())
- ['color']
- >>> list(session.values())
- ['red']
- >>> list(session.items())
- [('color', 'red')]
- >>> len(session)
- 1
- >>> [x for x in session]
- ['color']
- >>> del session['color']
- >>> session.get('color') is None
- True
- """
-
-def test_Session():
- """
- >>> request = setUp(PersistentSessionDataContainer)
- >>> request2 = HTTPRequest(None, None, {}, None)
-
- >>> ISession.providedBy(Session(request))
- True
-
- >>> session1 = Session(request)['products.foo']
- >>> session2 = Session(request)['products.bar']
- >>> session3 = Session(request)['products.bar'] # dupe
- >>> session4 = Session(request2)['products.bar'] # not dupe
-
- Make sure it returned sane values
-
- >>> ISessionData.providedBy(session1)
- True
- >>> ISessionData.providedBy(session2)
- True
- >>> session2 == session3
- True
- >>> ISessionData.providedBy(session4)
- True
-
- Make sure that product_ids don't share a namespace, except when they should
-
- >>> session1['color'] = 'red'
- >>> session2['color'] = 'blue'
- >>> session4['color'] = 'vomit'
- >>> session1['color']
- 'red'
- >>> session2['color']
- 'blue'
- >>> session3['color']
- 'blue'
- >>> session4['color']
- 'vomit'
-
- >>> tearDown()
- """
-
from zope.app.appsetup.tests import TestBootstrapSubscriberBase, EventStub
class TestBootstrapInstance(TestBootstrapSubscriberBase):
@@ -336,16 +66,31 @@
root_folder = root[ZopePublication.root_name]
setSite(root_folder)
- zapi.getUtility(IBrowserIdManager)
+ zapi.getUtility(IClientIdManager)
zapi.getUtility(ISessionDataContainer)
cx.close()
+# Test the code in our API documentation is correct
+def test_documentation():
+ pass
+test_documentation.__doc__ = '''
+ >>> request = setUp(RAMSessionDataContainer)
+ %s
+
+ >>> tearDown()
+
+ ''' % (open(
+ os.path.join(os.path.dirname(__file__), 'session.stx')
+ ).read(),)
+
def test_suite():
return unittest.TestSuite((
doctest.DocTestSuite(),
+ doctest.DocTestSuite('zope.app.session.session'),
+ doctest.DocTestSuite('zope.app.session.http'),
unittest.makeSuite(TestBootstrapInstance),
))
@@ -354,4 +99,3 @@
# vim: set filetype=python ts=4 sw=4 et si
-
More information about the Zope3-Checkins
mailing list