[Zope3-checkins] CVS: Zope3/src/zope/app/utilities - session.py:1.2
session.stx:1.2 configure.zcml:1.5
Stuart Bishop
zen at shangri-la.dropbear.id.au
Mon Feb 9 00:17:08 EST 2004
Update of /cvs-repository/Zope3/src/zope/app/utilities
In directory cvs.zope.org:/tmp/cvs-serv20093/src/zope/app/utilities
Modified Files:
configure.zcml
Added Files:
session.py session.stx
Log Message:
Session work to HEAD
=== Zope3/src/zope/app/utilities/session.py 1.1 => 1.2 ===
--- /dev/null Mon Feb 9 00:17:06 2004
+++ Zope3/src/zope/app/utilities/session.py Mon Feb 9 00:16:32 2004
@@ -0,0 +1,188 @@
+##############################################################################
+#
+# 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.
+#
+##############################################################################
+"""Simplistic session service implemented using cookies.
+
+This is more of a demonstration than a full implementation, but it should
+work.
+"""
+
+# System imports
+import sha, time, string, random, hmac, logging
+from UserDict import IterableUserDict
+
+# Zope3 imports
+from persistence import Persistent
+from persistence.dict import PersistentDict
+from zope.server.http.http_date import build_http_date
+from zope.component import getService
+from zope.interface import implements
+from zope.app import zapi
+from zodb.btrees.OOBTree import OOBTree
+
+# Sibling imports
+from zope.app.interfaces.utilities.session import \
+ IBrowserIdManager, IBrowserId, ICookieBrowserIdManager, \
+ ISessionDataContainer, ISession, IFullMapping
+from zope.app.interfaces.container import IContained
+
+cookieSafeTrans = string.maketrans("+/", "-.")
+
+def digestEncode(s):
+ """Encode SHA digest for cookie."""
+ return s.encode("base64")[:-2].translate(cookieSafeTrans)
+
+
+class BrowserId(str):
+ """A browser id"""
+ implements(IBrowserId)
+
+
+class CookieBrowserIdManager(Persistent):
+ """Session service implemented using cookies."""
+
+ implements(IBrowserIdManager, ICookieBrowserIdManager, IContained)
+
+ __parent__ = __name__ = None
+
+ def __init__(self):
+ self.namespace = "zope3_cs_%x" % (int(time.time()) - 1000000000)
+ self.secret = "%.20f" % random.random()
+ self.cookieLifeSeconds = 3600
+
+ 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 BrowserId(s + digestEncode(mac))
+
+ def getRequestId(self, request):
+ """Return the IBrowserId encoded in request 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 BrowserId(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.
+
+ # XXX: Fix this as per documentation in ICookieBrowserIdManager
+ if self.cookieLifeSeconds == 0 or self.cookieLifeSeconds is None:
+ raise NotImplementedError, \
+ 'Need to implement advanced cookie lifetime'
+
+ if self.cookieLifeSeconds:
+ expires = build_http_date(time.time() + self.cookieLifeSeconds)
+ else:
+ expires = None
+ request.response.setCookie(
+ self.namespace,
+ id,
+ expires=expires,
+ path=request.getApplicationURL(path_only=True)
+ )
+
+
+ #######################################
+ # Implementation of IBrowserIdManager #
+
+ def getBrowserId(self, request):
+ 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, IContained)
+
+ def __init__(self):
+ self.data = OOBTree()
+
+class SessionData(Persistent, IterableUserDict):
+ ''' Mapping nodes in the ISessionDataContainer tree '''
+ implements(IFullMapping)
+ def __init__(self):
+ self.data = OOBTree()
+ def __setitem__(self, key, value):
+ self.data[key] = value
+ self.data._p_changed = 1
+ def __delitem__(self, key):
+ del self.data[key]
+ self.data._p_changed = 1
+
+class Session(IterableUserDict):
+ implements(ISession)
+ def __init__(self, data_manager, browser_id, product_id):
+ browser_id = str(browser_id)
+ product_id = str(product_id)
+ try:
+ data = data_manager[browser_id]
+ except KeyError:
+ data_manager[browser_id] = SessionData()
+ data_manager[browser_id][product_id] = SessionData()
+ self.data = data_manager[browser_id][product_id]
+ else:
+ try:
+ self.data = data[product_id]
+ except KeyError:
+ data[product_id] = SessionData()
+ self.data = data[product_id]
+
+
+def getSession(context, request, product_id, session_data_container=None):
+ ''' Retrieve an ISession. session_data_container defaults to
+ an ISessionDataContainer utility registered with the name product_id
+ '''
+ if session_data_container is None:
+ dc = zapi.getUtility(context, ISessionDataContainer, product_id)
+ elif ISessionDataContainer.isImplementedBy(session_data_container):
+ dc = session_data_container
+ else:
+ dc = zapi.getUtility(
+ context, ISessionDataContainer, session_data_container
+ )
+
+ bim = zapi.getUtility(context, IBrowserIdManager)
+ browser_id = bim.getBrowserId(request)
+ return Session(dc, browser_id, product_id)
+
+
=== Zope3/src/zope/app/utilities/session.stx 1.1 => 1.2 ===
--- /dev/null Mon Feb 9 00:17:06 2004
+++ Zope3/src/zope/app/utilities/session.stx Mon Feb 9 00:16:32 2004
@@ -0,0 +1,47 @@
+Session Support
+---------------
+
+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)
+
+ISessionDataContainer Utilities provide a mapping interface to store
+session data. The ISessionDataContainer is responsible for expiring
+data.
+
+
+Python example::
+
+ >>> browser_id = getAdapter(request, IBrowserId))
+
+ >>> explicit_dm = getUtility(None, ISessionDataContainer,
+ ... 'zopeproducts.fooprod')
+ >>> session = Session(explicit_dm, browser_id, 'zopeproducts.foorprod')
+ >>> session['color'] = 'red'
+
+ or....
+
+ >>> session = zapi.getSession(request, 'zopeproducts.fooprod')
+ >>> session['color'] = 'red'
+
+Page Template example::
+
+ <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>
+
+TODO
+----
+Do we want to provide one or more 'default' ISessionDataContainer's out of the
+box (eg. 'persistant' and 'transient')?
+
=== Zope3/src/zope/app/utilities/configure.zcml 1.4 => 1.5 ===
--- Zope3/src/zope/app/utilities/configure.zcml:1.4 Sun Sep 21 13:33:48 2003
+++ Zope3/src/zope/app/utilities/configure.zcml Mon Feb 9 00:16:32 2004
@@ -1,4 +1,40 @@
-<configure xmlns="http://namespaces.zope.org/zope">
+<configure
+ xmlns="http://namespaces.zope.org/zope"
+ xmlns:browser="http://namespaces.zope.org/browser">
+
+ <!-- Session machinery -->
+
+ <content class=".session.CookieBrowserIdManager">
+ <implements
+ interface="zope.app.interfaces.services.utility.ILocalUtility" />
+ <implements
+ interface="zope.app.interfaces.annotation.IAttributeAnnotatable" />
+ <require
+ interface="zope.app.interfaces.utilities.session.ICookieBrowserIdManager"
+ permission="zope.Public" />
+ <require
+ set_schema="zope.app.interfaces.utilities.session.ICookieBrowserIdManager"
+ permission="zope.ManageContent" />
+ </content>
+
+ <content class=".session.SessionData">
+ <allow interface="zope.app.interfaces.utilities.session.IFullMapping" />
+ </content>
+
+ <content class=".session.PersistentSessionDataContainer">
+ <implements
+ interface="zope.app.interfaces.utilities.session.ISessionDataContainer"/>
+ <implements
+ interface="zope.app.interfaces.services.utility.ILocalUtility" />
+ <implements
+ interface="zope.app.interfaces.annotation.IAttributeAnnotatable" />
+ <require
+ interface="zope.app.interfaces.utilities.session.ISessionDataContainer"
+ permission="zope.Public" />
+ <require
+ set_schema="zope.app.interfaces.utilities.session.ISessionDataContainer"
+ permission="zope.ManageContent" />
+ </content>
<!-- Mutable Schema -->
More information about the Zope3-Checkins
mailing list