[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