Commit b33bf1df authored by Hanno Schlichting's avatar Hanno Schlichting

Move Products.Sessions and Transience into new distribution.

parent db473745
......@@ -21,6 +21,9 @@ Features Added
Restructuring
+++++++++++++
- Create new `Products.Sessions` distribution including Products.Sessions
and Products.Transience code.
- Merge `Products.OFSP` project back in.
- Dropped dependency declarations for indirect dependencies:
......
......@@ -65,6 +65,7 @@ eggs =
Products.ExternalMethod
Products.MailHost
Products.PythonScripts
Products.Sessions
Products.SiteErrorLog
Products.StandardCacheManagers
Products.ZCatalog
......
......@@ -24,6 +24,7 @@ Products.BTreeFolder2 = git ${remotes:github}/Products.BTreeFolder2 pushurl=${re
Products.ExternalMethod = git ${remotes:github}/Products.ExternalMethod pushurl=${remotes:github_push}/Products.ExternalMethod
Products.MailHost = git ${remotes:github}/Products.MailHost pushurl=${remotes:github_push}/Products.MailHost
Products.PythonScripts = git ${remotes:github}/Products.PythonScripts pushurl=${remotes:github_push}/Products.PythonScripts
Products.Sessions = git ${remotes:github}/Products.Sessions.git pushurl=${remotes:github_push}/Products.Sessions
Products.SiteErrorLog = git ${remotes:github}/Products.SiteErrorLog pushurl=${remotes:github_push}/Products.SiteErrorLog
Products.StandardCacheManagers = git ${remotes:github}/Products.StandardCacheManagers pushurl=${remotes:github_push}/Products.StandardCacheManagers
Products.ZCatalog = git ${remotes:github}/Products.ZCatalog pushurl=${remotes:github_push}/Products.ZCatalog branch=master
......
############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
############################################################################
import binascii
from cgi import escape
from hashlib import sha256
import logging
import os
import re
import string
import sys
import time
from urllib import quote
from urlparse import urlparse
from urlparse import urlunparse
from AccessControl.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo
from Acquisition import Implicit
from Acquisition import aq_parent
from Acquisition import aq_inner
from App.Management import Tabs
from App.special_dtml import DTMLFile
from Persistence import Persistent
from persistent import TimeStamp
from OFS.owner import Owned
from OFS.role import RoleManager
from OFS.SimpleItem import Item
from ZPublisher.BeforeTraverse import registerBeforeTraverse
from ZPublisher.BeforeTraverse import unregisterBeforeTraverse
from ZPublisher.BeforeTraverse import queryBeforeTraverse
from zope.interface import implements
from Products.Sessions.interfaces import IBrowserIdManager
from Products.Sessions.interfaces import BrowserIdManagerErr
from Products.Sessions.SessionPermissions import ACCESS_CONTENTS_PERM
from Products.Sessions.SessionPermissions import CHANGE_IDMGR_PERM
from Products.Sessions.SessionPermissions import MGMT_SCREEN_PERM
b64_trans = string.maketrans('+/', '-.')
b64_untrans = string.maketrans('-.', '+/')
badidnamecharsin = re.compile('[\?&;,<> ]').search
badcookiecharsin = re.compile('[;,<>& ]').search
twodotsin = re.compile('(\w*\.){2,}').search
_marker = []
constructBrowserIdManagerForm = DTMLFile('dtml/addIdManager', globals())
BROWSERID_MANAGER_NAME = 'browser_id_manager'# imported by SessionDataManager
ALLOWED_BID_NAMESPACES = ('form', 'cookies', 'url')
ADD_BROWSER_ID_MANAGER_PERM="Add Browser Id Manager"
TRAVERSAL_APPHANDLE = 'BrowserIdManager'
LOG = logging.getLogger('Zope.BrowserIdManager')
# Use the system PRNG if possible
import random
try:
random = random.SystemRandom()
using_sysrandom = True
except NotImplementedError:
using_sysrandom = False
def _randint(start, end):
if not using_sysrandom:
# This is ugly, and a hack, but it makes things better than
# the alternative of predictability. This re-seeds the PRNG
# using a value that is hard for an attacker to predict, every
# time a random string is required. This may change the
# properties of the chosen random sequence slightly, but this
# is better than absolute predictability.
random.seed(sha256(
"%s%s%s" % (random.getstate(), time.time(), os.getpid())
).digest())
return random.randint(start, end)
def constructBrowserIdManager(
self, id=BROWSERID_MANAGER_NAME, title='', idname='_ZopeId',
location=('cookies', 'form'), cookiepath='/', cookiedomain='',
cookielifedays=0, cookiesecure=0, cookiehttponly=0, auto_url_encoding=0,
REQUEST=None
):
""" """
ob = BrowserIdManager(id, title, idname, location, cookiepath,
cookiedomain, cookielifedays, cookiesecure,
cookiehttponly, auto_url_encoding)
self._setObject(id, ob)
ob = self._getOb(id)
if REQUEST is not None:
return self.manage_main(self, REQUEST, update_menu=1)
class BrowserIdManager(Item, Persistent, Implicit, RoleManager, Owned, Tabs):
""" browser id management class
"""
implements(IBrowserIdManager)
meta_type = 'Browser Id Manager'
security = ClassSecurityInfo()
security.declareObjectPublic()
ok = {'meta_type': 1, 'id': 1, 'title': 1, 'icon': 1, 'title_or_id': 1}
security.setDefaultAccess(ok)
security.setPermissionDefault(MGMT_SCREEN_PERM, ['Manager'])
security.setPermissionDefault(ACCESS_CONTENTS_PERM,['Manager','Anonymous'])
security.setPermissionDefault(CHANGE_IDMGR_PERM, ['Manager'])
# BBB
auto_url_encoding = 0
cookie_http_only = 0
def __init__(self, id, title='', idname='_ZopeId',
location=('cookies', 'form'), cookiepath=('/'),
cookiedomain='', cookielifedays=0, cookiesecure=0,
cookiehttponly=0, auto_url_encoding=0):
self.id = str(id)
self.title = str(title)
self.setBrowserIdName(idname)
self.setBrowserIdNamespaces(location)
self.setCookiePath(cookiepath)
self.setCookieDomain(cookiedomain)
self.setCookieLifeDays(cookielifedays)
self.setCookieSecure(cookiesecure)
self.setCookieHTTPOnly(cookiehttponly)
self.setAutoUrlEncoding(auto_url_encoding)
# IBrowserIdManager
security.declareProtected(ACCESS_CONTENTS_PERM, 'hasBrowserId')
def hasBrowserId(self):
""" See IBrowserIdManager.
"""
try:
return self.getBrowserId(create=0) is not None
except BrowserIdManagerErr:
return False
security.declareProtected(ACCESS_CONTENTS_PERM, 'getBrowserId')
def getBrowserId(self, create=1):
""" See IBrowserIdManager.
"""
REQUEST = self.REQUEST
# let's see if bid has already been attached to request
bid = getattr(REQUEST, 'browser_id_', None)
if bid is not None:
# it's already set in this request so we can just return it
# if it's well-formed
if not isAWellFormedBrowserId(bid):
# somebody screwed with the REQUEST instance during
# this request.
raise BrowserIdManagerErr(
'Ill-formed browserid in '
'REQUEST.browser_id_: %s' % escape(bid))
return bid
# fall through & ck form/cookie namespaces if bid is not in request.
tk = self.browserid_name
ns = self.browserid_namespaces
for name in ns:
if name == 'url':
continue # browser ids in url are checked by Traverser class
current_ns = getattr(REQUEST, name, None)
if current_ns is None:
continue
bid = current_ns.get(tk, None)
if bid is not None:
# hey, we got a browser id!
if isAWellFormedBrowserId(bid):
# bid is not "plain old broken"
REQUEST.browser_id_ = bid
REQUEST.browser_id_ns_ = name
return bid
# fall through if bid is invalid or not in namespaces
if create:
# create a brand new bid
bid = getNewBrowserId()
if 'cookies' in ns:
self._setCookie(bid, REQUEST)
REQUEST.browser_id_ = bid
REQUEST.browser_id_ns_ = None
return bid
# implies a return of None if:
# (not create=1) and (invalid or ((not in req) and (not in ns)))
security.declareProtected(ACCESS_CONTENTS_PERM, 'getBrowserIdName')
def getBrowserIdName(self):
""" See IBrowserIdManager.
"""
return self.browserid_name
security.declareProtected(ACCESS_CONTENTS_PERM, 'isBrowserIdNew')
def isBrowserIdNew(self):
""" See IBrowserIdManager.
"""
if not self.getBrowserId(create=False):
raise BrowserIdManagerErr('There is no current browser id.')
# ns will be None if new
return getattr(self.REQUEST, 'browser_id_ns_', None) is None
security.declareProtected(ACCESS_CONTENTS_PERM, 'isBrowserIdFromCookie')
def isBrowserIdFromCookie(self):
""" See IBrowserIdManager.
"""
if not self.getBrowserId(create=False):
raise BrowserIdManagerErr('There is no current browser id.')
if getattr(self.REQUEST, 'browser_id_ns_') == 'cookies':
return 1
security.declareProtected(ACCESS_CONTENTS_PERM, 'isBrowserIdFromForm')
def isBrowserIdFromForm(self):
""" See IBrowserIdManager.
"""
if not self.getBrowserId(create=False):
raise BrowserIdManagerErr('There is no current browser id.')
if getattr(self.REQUEST, 'browser_id_ns_') == 'form':
return 1
security.declareProtected(ACCESS_CONTENTS_PERM, 'isBrowserIdFromUrl')
def isBrowserIdFromUrl(self):
""" See IBrowserIdManager.
"""
if not self.getBrowserId(create=False):
raise BrowserIdManagerErr('There is no current browser id.')
if getattr(self.REQUEST, 'browser_id_ns_') == 'url':
return 1
security.declareProtected(ACCESS_CONTENTS_PERM, 'flushBrowserIdCookie')
def flushBrowserIdCookie(self):
""" See IBrowserIdManager.
"""
if 'cookies' not in self.browserid_namespaces:
raise BrowserIdManagerErr(
'Cookies are not now being used as a '
'browser id namespace, thus the '
'browserid cookie cannot be flushed.')
self._setCookie('deleted', self.REQUEST, remove=1)
security.declareProtected(ACCESS_CONTENTS_PERM,'setBrowserIdCookieByForce')
def setBrowserIdCookieByForce(self, bid):
""" See IBrowserIdManager.
"""
if 'cookies' not in self.browserid_namespaces:
raise BrowserIdManagerErr(
'Cookies are not now being used as a '
'browser id namespace, thus the '
'browserid cookie cannot be forced.')
self._setCookie(bid, self.REQUEST)
security.declareProtected(ACCESS_CONTENTS_PERM, 'getHiddenFormField')
def getHiddenFormField(self):
""" See IBrowserIdManager.
"""
s = '<input type="hidden" name="%s" value="%s" />'
return s % (self.getBrowserIdName(), self.getBrowserId())
security.declareProtected(ACCESS_CONTENTS_PERM, 'encodeUrl')
def encodeUrl(self, url, style='querystring', create=1):
# See IBrowserIdManager
bid = self.getBrowserId(create)
if bid is None:
raise BrowserIdManagerErr('There is no current browser id.')
name = self.getBrowserIdName()
if style == 'querystring': # encode bid in querystring
if '?' in url:
return '%s&amp;%s=%s' % (url, name, bid)
else:
return '%s?%s=%s' % (url, name, bid)
else: # encode bid as first two URL path segments
proto, host, path, params, query, frag = urlparse(url)
path = '/%s/%s%s' % (name, bid, path)
return urlunparse((proto, host, path, params, query, frag))
# Non-IBrowserIdManager accessors / mutators.
security.declareProtected(CHANGE_IDMGR_PERM, 'setBrowserIdName')
def setBrowserIdName(self, k):
""" Set browser id name string
o Enforce "valid" values.
"""
if not (type(k) is type('') and k and not badidnamecharsin(k)):
raise BrowserIdManagerErr(
'Bad id name string %s' % escape(repr(k)))
self.browserid_name = k
security.declareProtected(CHANGE_IDMGR_PERM, 'setBrowserIdNamespaces')
def setBrowserIdNamespaces(self, ns):
"""
accepts list of allowable browser id namespaces
"""
for name in ns:
if name not in ALLOWED_BID_NAMESPACES:
raise BrowserIdManagerErr(
'Bad browser id namespace %s' % repr(name))
self.browserid_namespaces = tuple(ns)
security.declareProtected(ACCESS_CONTENTS_PERM, 'getBrowserIdNamespaces')
def getBrowserIdNamespaces(self):
""" """
return self.browserid_namespaces
security.declareProtected(CHANGE_IDMGR_PERM, 'setCookiePath')
def setCookiePath(self, path=''):
""" sets cookie 'path' element for id cookie """
if not (type(path) is type('') and not badcookiecharsin(path)):
raise BrowserIdManagerErr(
'Bad cookie path %s' % escape(repr(path)))
self.cookie_path = path
security.declareProtected(ACCESS_CONTENTS_PERM, 'getCookiePath')
def getCookiePath(self):
""" """
return self.cookie_path
security.declareProtected(CHANGE_IDMGR_PERM, 'setCookieLifeDays')
def setCookieLifeDays(self, days):
""" offset for id cookie 'expires' element """
if type(days) not in (type(1), type(1.0)):
raise BrowserIdManagerErr(
'Bad cookie lifetime in days %s '
'(requires integer value)' % escape(repr(days)))
self.cookie_life_days = int(days)
security.declareProtected(ACCESS_CONTENTS_PERM, 'getCookieLifeDays')
def getCookieLifeDays(self):
""" """
return self.cookie_life_days
security.declareProtected(CHANGE_IDMGR_PERM, 'setCookieDomain')
def setCookieDomain(self, domain):
""" sets cookie 'domain' element for id cookie """
if type(domain) is not type(''):
raise BrowserIdManagerErr(
'Cookie domain must be string: %s'
% escape(repr(domain)))
if not domain:
self.cookie_domain = ''
return
if not twodotsin(domain):
raise BrowserIdManagerErr(
'Cookie domain must contain at least two dots '
'(e.g. ".zope.org" or "www.zope.org") or it must '
'be left blank. : ' '%s' % escape(`domain`))
if badcookiecharsin(domain):
raise BrowserIdManagerErr(
'Bad characters in cookie domain %s'
% escape(`domain`))
self.cookie_domain = domain
security.declareProtected(ACCESS_CONTENTS_PERM, 'getCookieDomain')
def getCookieDomain(self):
""" """
return self.cookie_domain
security.declareProtected(CHANGE_IDMGR_PERM, 'setCookieHTTPOnly')
def setCookieHTTPOnly(self, http_only):
""" sets cookie 'HTTPOnly' on or off """
self.cookie_http_only = bool(http_only)
security.declareProtected(ACCESS_CONTENTS_PERM, 'getCookieHTTPOnly')
def getCookieHTTPOnly(self):
""" retrieve the 'HTTPOnly' flag """
return self.cookie_http_only
security.declareProtected(CHANGE_IDMGR_PERM, 'setCookieSecure')
def setCookieSecure(self, secure):
""" sets cookie 'secure' element for id cookie """
self.cookie_secure = not not secure
security.declareProtected(ACCESS_CONTENTS_PERM, 'getCookieSecure')
def getCookieSecure(self):
""" """
return self.cookie_secure
security.declareProtected(CHANGE_IDMGR_PERM, 'setAutoUrlEncoding')
def setAutoUrlEncoding(self, auto_url_encoding):
""" sets 'auto url encoding' on or off """
self.auto_url_encoding = not not auto_url_encoding
security.declareProtected(ACCESS_CONTENTS_PERM, 'getAutoUrlEncoding')
def getAutoUrlEncoding(self):
""" """
return self.auto_url_encoding
security.declareProtected(ACCESS_CONTENTS_PERM, 'isUrlInBidNamespaces')
def isUrlInBidNamespaces(self):
""" Returns true if 'url' is in the browser id namespaces
for this browser id """
return 'url' in self.browserid_namespaces
def _setCookie(
self, bid, REQUEST, remove=0, now=time.time, strftime=time.strftime,
gmtime=time.gmtime
):
""" """
expires = None
if remove:
expires = "Sun, 10-May-1971 11:59:00 GMT"
elif self.cookie_life_days:
expires = now() + self.cookie_life_days * 86400
# Wdy, DD-Mon-YYYY HH:MM:SS GMT
expires = strftime('%a %d-%b-%Y %H:%M:%S GMT',gmtime(expires))
# cookie attributes managed by BrowserIdManager
d = {'domain':self.cookie_domain,'path':self.cookie_path,
'secure':self.cookie_secure,'http_only': self.cookie_http_only,
'expires':expires}
if self.cookie_secure:
URL1 = REQUEST.get('URL1', None)
if URL1 is None:
return # should we raise an exception?
if string.split(URL1,':')[0] != 'https':
return # should we raise an exception?
cookies = REQUEST.RESPONSE.cookies
cookie = cookies[self.browserid_name]= {}
for k,v in d.items():
if v:
cookie[k] = v #only stuff things with true values
cookie['value'] = bid
def _setId(self, id):
if id != self.id:
raise ValueError('Cannot rename a browser id manager')
# Jukes for handling URI-munged browser IDS
def hasTraversalHook(self, parent):
name = TRAVERSAL_APPHANDLE
return not not queryBeforeTraverse(parent, name)
def updateTraversalData(self):
if 'url' in self.browserid_namespaces:
self.registerTraversalHook()
else:
self.unregisterTraversalHook()
def unregisterTraversalHook(self):
parent = aq_parent(aq_inner(self))
name = TRAVERSAL_APPHANDLE
if self.hasTraversalHook(parent):
unregisterBeforeTraverse(parent, name)
def registerTraversalHook(self):
parent = aq_parent(aq_inner(self))
if not self.hasTraversalHook(parent):
hook = BrowserIdManagerTraverser()
name = TRAVERSAL_APPHANDLE
priority = 40 # "higher" priority than session data traverser
registerBeforeTraverse(parent, hook, name, priority)
# ZMI
manage_options=({'label': 'Settings', 'action':'manage_browseridmgr'},
{'label': 'Security', 'action':'manage_access'},
{'label': 'Ownership', 'action':'manage_owner'},
)
def manage_afterAdd(self, item, container):
""" Maybe add our traversal hook """
self.updateTraversalData()
def manage_beforeDelete(self, item, container):
""" Remove our traversal hook if it exists """
self.unregisterTraversalHook()
security.declareProtected(MGMT_SCREEN_PERM, 'manage_browseridmgr')
manage_browseridmgr = DTMLFile('dtml/manageIdManager', globals())
security.declareProtected(CHANGE_IDMGR_PERM,
'manage_changeBrowserIdManager')
def manage_changeBrowserIdManager(
self, title='', idname='_ZopeId', location=('cookies', 'form'),
cookiepath='/', cookiedomain='', cookielifedays=0, cookiesecure=0,
cookiehttponly=0, auto_url_encoding=0, REQUEST=None
):
""" """
self.title = str(title)
self.setBrowserIdName(idname)
self.setCookiePath(cookiepath)
self.setCookieDomain(cookiedomain)
self.setCookieLifeDays(cookielifedays)
self.setCookieSecure(cookiesecure)
self.setCookieHTTPOnly(cookiehttponly)
self.setBrowserIdNamespaces(location)
self.setAutoUrlEncoding(auto_url_encoding)
self.updateTraversalData()
if REQUEST is not None:
msg = '/manage_browseridmgr?manage_tabs_message=Changes saved'
REQUEST.RESPONSE.redirect(self.absolute_url()+msg)
InitializeClass(BrowserIdManager)
class BrowserIdManagerTraverser(Persistent):
def __call__(self, container, request, browser_id=None,
browser_id_ns=None,
BROWSERID_MANAGER_NAME=BROWSERID_MANAGER_NAME):
"""
Registered hook to set and get a browser id in the URL. If
a browser id is found in the URL of an incoming request, we put it
into a place where it can be found later by the browser id manager.
If our browser id manager's auto-url-encoding feature is on, cause
Zope-generated URLs to contain the browser id by rewriting the
request._script list.
"""
browser_id_manager = getattr(container, BROWSERID_MANAGER_NAME, None)
# fail if we cannot find a browser id manager (that means this
# instance has been "orphaned" somehow)
if browser_id_manager is None:
LOG.error('Could not locate browser id manager!')
return
try:
stack = request['TraversalRequestNameStack']
request.browser_id_ns_ = browser_id_ns
bid_name = browser_id_manager.getBrowserIdName()
# stuff the browser id and id namespace into the request
# if the URL has a browser id name and browser id as its first
# two elements. Only remove these elements from the
# traversal stack if they are a "well-formed pair".
if len(stack) >= 2 and stack[-1] == bid_name:
if isAWellFormedBrowserId(stack[-2]):
name = stack.pop() # pop the name off the stack
browser_id = stack.pop() # pop id off the stack
request.browser_id_ = browser_id
request.browser_id_ns_ = 'url'
# if the browser id manager is set up with 'auto url encoding',
# cause generated URLs to be encoded with the browser id name/value
# pair by munging request._script.
if browser_id_manager.getAutoUrlEncoding():
if browser_id is None:
request.browser_id_ = browser_id = getNewBrowserId()
request._script.append(quote(bid_name))
request._script.append(quote(browser_id))
except:
LOG.error('indeterminate error', exc_info=sys.exc_info())
def getB64TStamp(
b2a=binascii.b2a_base64,gmtime=time.gmtime, time=time.time,
b64_trans=b64_trans, split=string.split,
TimeStamp=TimeStamp.TimeStamp, translate=string.translate
):
t=time()
ts=split(b2a(`TimeStamp(*gmtime(t)[:5]+(t%60,))`)[:-1],'=')[0]
return translate(ts, b64_trans)
def getB64TStampToInt(
ts, TimeStamp=TimeStamp.TimeStamp, b64_untrans=b64_untrans,
a2b=binascii.a2b_base64, translate=string.translate
):
return TimeStamp(a2b(translate(ts+'=',b64_untrans))).timeTime()
def getBrowserIdPieces(bid):
""" returns browser id parts in a tuple consisting of rand_id,
timestamp
"""
return (bid[:8], bid[8:19])
def isAWellFormedBrowserId(bid, binerr=binascii.Error):
try:
rnd, ts = getBrowserIdPieces(bid)
int(rnd)
getB64TStampToInt(ts)
return bid
except (TypeError, ValueError, AttributeError, IndexError, binerr):
return None
def getNewBrowserId(randint=_randint, maxint=99999999):
""" Returns 19-character string browser id
'AAAAAAAABBBBBBBB'
where:
A == leading-0-padded 8-char string-rep'd random integer
B == modified base64-encoded 11-char timestamp
To be URL-compatible, base64 encoding is modified as follows:
'=' end-padding is stripped off
'+' is translated to '-'
'/' is translated to '.'
An example is: 89972317A0C3EHnUi90w
"""
return '%08i%s' % (randint(0, maxint - 1), getB64TStamp())
############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
############################################################################
from logging import getLogger
import re
import sys
from AccessControl.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo
from Acquisition import Implicit
from App.special_dtml import DTMLFile
from App.Management import Tabs
from OFS.owner import Owned
from OFS.role import RoleManager
from OFS.SimpleItem import Item
from Persistence import Persistent
from ZPublisher.BeforeTraverse import registerBeforeTraverse
from ZPublisher.BeforeTraverse import unregisterBeforeTraverse
from ZODB.POSException import ConflictError
from zope.interface import implements
from Products.Sessions.interfaces import ISessionDataManager
from Products.Sessions.interfaces import SessionDataManagerErr
from Products.Sessions.SessionPermissions import ACCESS_CONTENTS_PERM
from Products.Sessions.SessionPermissions import ACCESS_SESSIONDATA_PERM
from Products.Sessions.SessionPermissions import ARBITRARY_SESSIONDATA_PERM
from Products.Sessions.SessionPermissions import CHANGE_DATAMGR_PERM
from Products.Sessions.SessionPermissions import MGMT_SCREEN_PERM
from Products.Sessions.common import DEBUG
from Products.Sessions.BrowserIdManager import BROWSERID_MANAGER_NAME
bad_path_chars_in=re.compile('[^a-zA-Z0-9-_~\,\. \/]').search
LOG = getLogger('SessionDataManager')
constructSessionDataManagerForm = DTMLFile('dtml/addDataManager',
globals())
ADD_SESSION_DATAMANAGER_PERM="Add Session Data Manager"
def constructSessionDataManager(self, id, title='', path=None,
requestName=None, REQUEST=None):
""" """
ob = SessionDataManager(id, path, title, requestName)
self._setObject(id, ob)
if REQUEST is not None:
return self.manage_main(self, REQUEST, update_menu=1)
class SessionIdManagerErr(Exception):
pass
class SessionDataManager(Item, Implicit, Persistent, RoleManager, Owned, Tabs):
""" The Zope default session data manager implementation """
meta_type = 'Session Data Manager'
manage_options=(
{'label': 'Settings',
'action':'manage_sessiondatamgr',
},
{'label': 'Security',
'action':'manage_access',
},
{'label': 'Ownership',
'action':'manage_owner'
},
)
security = ClassSecurityInfo()
security.declareObjectPublic()
ok = {'meta_type': 1, 'id': 1, 'title': 1, 'icon': 1, 'title_or_id': 1}
security.setDefaultAccess(ok)
security.setPermissionDefault(CHANGE_DATAMGR_PERM, ['Manager'])
security.setPermissionDefault(MGMT_SCREEN_PERM, ['Manager'])
security.setPermissionDefault(ACCESS_CONTENTS_PERM,['Manager','Anonymous'])
security.setPermissionDefault(ARBITRARY_SESSIONDATA_PERM,['Manager'])
security.setPermissionDefault(ACCESS_SESSIONDATA_PERM,
['Manager','Anonymous'])
implements(ISessionDataManager)
manage_sessiondatamgr = DTMLFile('dtml/manageDataManager',
globals())
# INTERFACE METHODS FOLLOW
security.declareProtected(ACCESS_SESSIONDATA_PERM, 'getSessionData')
def getSessionData(self, create=1):
""" """
key = self.getBrowserIdManager().getBrowserId(create=create)
if key is not None:
return self._getSessionDataObject(key)
security.declareProtected(ACCESS_SESSIONDATA_PERM, 'hasSessionData')
def hasSessionData(self):
""" """
key = self.getBrowserIdManager().getBrowserId(create=0)
if key is None:
return 0
return self._hasSessionDataObject(key)
security.declareProtected(ARBITRARY_SESSIONDATA_PERM,'getSessionDataByKey')
def getSessionDataByKey(self, key):
return self._getSessionDataObjectByKey(key)
security.declareProtected(ACCESS_CONTENTS_PERM, 'getBrowserIdManager')
def getBrowserIdManager(self):
""" """
mgr = getattr(self, BROWSERID_MANAGER_NAME, None)
if mgr is None:
raise SessionDataManagerErr(
'No browser id manager named %s could be found.' %
BROWSERID_MANAGER_NAME
)
return mgr
# END INTERFACE METHODS
def __init__(self, id, path=None, title='', requestName=None):
self.id = id
self.setContainerPath(path)
self.setTitle(title)
self._requestSessionName = requestName
security.declareProtected(CHANGE_DATAMGR_PERM, 'manage_changeSDM')
def manage_changeSDM(self, title, path=None, requestName=None,
REQUEST=None):
""" """
self.setContainerPath(path)
self.setTitle(title)
if requestName:
if requestName != self._requestSessionName:
self.updateTraversalData(requestName)
else:
self.updateTraversalData(None)
if REQUEST is not None:
return self.manage_sessiondatamgr(
self, REQUEST, manage_tabs_message = 'Changes saved.'
)
security.declareProtected(CHANGE_DATAMGR_PERM, 'setTitle')
def setTitle(self, title):
""" """
if not title: self.title = ''
else: self.title = str(title)
security.declareProtected(CHANGE_DATAMGR_PERM, 'setContainerPath')
def setContainerPath(self, path=None):
""" """
if not path:
self.obpath = None # undefined state
elif type(path) is type(''):
if bad_path_chars_in(path):
raise SessionDataManagerErr(
'Container path contains characters invalid in a Zope '
'object path'
)
self.obpath = path.split('/')
elif type(path) in (type([]), type(())):
self.obpath = list(path) # sequence
else:
raise SessionDataManagerErr('Bad path value %s' % path)
security.declareProtected(MGMT_SCREEN_PERM, 'getContainerPath')
def getContainerPath(self):
""" """
if self.obpath is not None:
return '/'.join(self.obpath)
return '' # blank string represents undefined state
def _hasSessionDataObject(self, key):
""" """
c = self._getSessionDataContainer()
return c.has_key(key)
def _getSessionDataObject(self, key):
""" returns new or existing session data object """
container = self._getSessionDataContainer()
ob = container.new_or_existing(key)
# hasattr hides conflicts; be explicit by comparing to None
# because otherwise __len__ of the requested object might be called!
if ( getattr(ob, '__of__', None) is not None and
getattr(ob, 'aq_parent', None) is not None ):
# splice ourselves into the acquisition chain
return ob.__of__(self.__of__(ob.aq_parent))
return ob.__of__(self)
def _getSessionDataObjectByKey(self, key):
""" returns new or existing session data object """
container = self._getSessionDataContainer()
ob = container.get(key)
if ob is not None:
# hasattr hides conflicts; be explicit by comparing to None
# because otherwise __len__ of the requested object might be
# called!
if ( getattr(ob, '__of__', None) is not None and
getattr(ob, 'aq_parent', None) is not None ):
# splice ourselves into the acquisition chain
return ob.__of__(self.__of__(ob.aq_parent))
return ob.__of__(self)
def _getSessionDataContainer(self):
""" Do not cache the results of this call. Doing so breaks the
transactions for mounted storages. """
if self.obpath is None:
err = 'Session data container is unspecified in %s' % self.getId()
LOG.warn(err)
raise SessionIdManagerErr(err)
try:
# This should arguably use restrictedTraverse, but it
# currently fails for mounted storages. This might
# be construed as a security hole, albeit a minor one.
# unrestrictedTraverse is also much faster.
# hasattr hides conflicts
if DEBUG and not getattr(self, '_v_wrote_dc_type', None):
args = '/'.join(self.obpath)
LOG.debug('External data container at %s in use' % args)
self._v_wrote_dc_type = 1
return self.unrestrictedTraverse(self.obpath)
except ConflictError:
raise
except:
raise SessionDataManagerErr(
"External session data container '%s' not found." %
'/'.join(self.obpath)
)
security.declareProtected(MGMT_SCREEN_PERM, 'getRequestName')
def getRequestName(self):
""" """
return self._requestSessionName or ''
def manage_afterAdd(self, item, container):
""" Add our traversal hook """
self.updateTraversalData(self._requestSessionName)
def manage_beforeDelete(self, item, container):
""" Clean up on delete """
self.updateTraversalData(None)
def updateTraversalData(self, requestSessionName=None):
# Note this cant be called directly at add -- manage_afterAdd will
# work though.
parent = self.aq_inner.aq_parent
if getattr(self,'_hasTraversalHook', None):
unregisterBeforeTraverse(parent, 'SessionDataManager')
del self._hasTraversalHook
self._requestSessionName = None
if requestSessionName:
hook = SessionDataManagerTraverser(requestSessionName, self.id)
registerBeforeTraverse(parent, hook, 'SessionDataManager', 50)
self._hasTraversalHook = 1
self._requestSessionName = requestSessionName
InitializeClass(SessionDataManager)
class SessionDataManagerTraverser(Persistent):
def __init__(self, requestSessionName, sessionDataManagerName):
self._requestSessionName = requestSessionName
self._sessionDataManager = sessionDataManagerName
def __call__(self, container, request):
"""
This method places a session data object reference in
the request. It is called on each and every request to Zope in
Zopes after 2.5.0 when there is a session data manager installed
in the root.
"""
try:
sdmName = self._sessionDataManager
if not isinstance(sdmName, str):
# Zopes v2.5.0 - 2.5.1b1 stuck the actual session data
# manager object in _sessionDataManager in order to use
# its getSessionData method. We don't actually want to
# do this, because it's safer to use getattr to get the
# data manager object by name. Using getattr also puts
# the sdm in the right context automatically. Here we
# pay the penance for backwards compatibility:
sdmName = sdmName.id
sdm = getattr(container, sdmName)
getSessionData = sdm.getSessionData
except:
msg = 'Session automatic traversal failed to get session data'
LOG.warn(msg, exc_info=sys.exc_info())
return
# set the getSessionData method in the "lazy" namespace
if self._requestSessionName is not None:
request.set_lazy(self._requestSessionName, getSessionData)
############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
############################################################################
# BBB location for APIs now defined in Products.Sessions.interfaces
from Products.Sessions.interfaces import IBrowserIdManager
BrowserIdManagerInterface = IBrowserIdManager # BBB
from Products.Sessions.interfaces import ISessionDataManager
SessionDataManagerInterface = ISessionDataManager
from Products.Sessions.interfaces import SessionDataManagerErr
from Products.Sessions.interfaces import BrowserIdManagerErr
############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
############################################################################
CHANGE_DATAMGR_PERM = 'Change Session Data Manager'
MGMT_SCREEN_PERM = 'View management screens'
ACCESS_CONTENTS_PERM = 'Access contents information'
ACCESS_SESSIONDATA_PERM = 'Access session data'
ARBITRARY_SESSIONDATA_PERM = 'Access arbitrary user session data'
CHANGE_IDMGR_PERM = 'Change Browser Id Manager'
MANAGE_CONTAINER_PERM = 'Manage Session Data Container'
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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 managemnt product initialization
"""
from Products.Sessions.interfaces import BrowserIdManagerErr #BBB
from Products.Sessions.interfaces import SessionDataManagerErr #BBB
def initialize(context):
import BrowserIdManager
import SessionDataManager
context.registerClass(
BrowserIdManager.BrowserIdManager,
permission=BrowserIdManager.ADD_BROWSER_ID_MANAGER_PERM,
constructors=(BrowserIdManager.constructBrowserIdManagerForm,
BrowserIdManager.constructBrowserIdManager)
)
context.registerClass(
SessionDataManager.SessionDataManager,
permission=SessionDataManager.ADD_SESSION_DATAMANAGER_PERM,
constructors=(SessionDataManager.constructSessionDataManagerForm,
SessionDataManager.constructSessionDataManager)
)
# do module security declarations so folks can use some of the
# module-level stuff in PythonScripts
#
# declare on behalf of Transience too, since ModuleSecurityInfo is too
# stupid for me to declare in two places without overwriting one set
# with the other. :-(
from AccessControl import ModuleSecurityInfo
security = ModuleSecurityInfo('Products')
security.declarePublic('Sessions')
security.declarePublic('Transience')
security = ModuleSecurityInfo('Products.Sessions.interfaces')
security.declareObjectPublic()
security.setDefaultAccess('allow')
security = ModuleSecurityInfo('Products.Transience')
security.declarePublic('MaxTransientObjectsExceeded')
#BBB for names which should be imported from Products.Sessions.interfaces
security = ModuleSecurityInfo('Products.Sessions')
security.declarePublic('BrowserIdManagerErr')
security.declarePublic('SessionDataManagerErr')
############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
############################################################################
import os
DEBUG = os.environ.get('CST_DEBUG', '')
<configure xmlns="http://namespaces.zope.org/zope"
xmlns:five="http://namespaces.zope.org/five">
<five:deprecatedManageAddDelete
class="Products.Sessions.BrowserIdManager.BrowserIdManager"/>
<five:deprecatedManageAddDelete
class="Products.Sessions.SessionDataManager.SessionDataManager"/>
</configure>
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add Session Data Manager'
)">
<FORM ACTION="constructSessionDataManager" METHOD="POST">
<TABLE CELLSPACING="2">
<tr>
<div class="form-help">
Zope Session Data Managers objects keep track of your users' session data
objects. Developers interact with a Session Data Manager in order to store
and retrieve information during a user session. A Session Data Manager
communicates with a Browser Id Manager to determine the session information
for the current user, and hands out Session Data Objects related to that
user.
</div>
</tr>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Id
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="id" SIZE="20">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Title
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="title" SIZE="40">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Transient Object Container Path
</div>
<div class="form-help">e.g. '/temp_folder/session_data'.</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="path" SIZE="60" value="">
</TD>
</TR>
<tr>
<td align="LEFT" valign="TOP">
<div class="form-label">
Place SESSION in REQUEST object as
</div>
</td>
<td align="LEFT" valign="TOP">
<input class="form-element" type="TEXT" name="requestName"
value="SESSION">
</td>
</tr>
<tr>
</TR>
<TR>
<TD>
</TD>
<TD> <BR><INPUT class="form-element" TYPE="SUBMIT" VALUE=" Add "> </TD>
</TR>
</TABLE>
</FORM>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add Browser Id Manager'
)">
<FORM ACTION="constructBrowserIdManager" METHOD="POST">
<TABLE CELLSPACING="2">
<tr>
<td>&nbsp;</td>
</tr>
<tr>
<div class="form-help">
Zope Browser Id Manager objects allow Zope to differentiate between site
visitors by "tagging" each of their browsers with a unique identifier. This
is useful if you need to tell visitors apart from one another even if they do
not "log in" to your site. Browser Id Managers are generally used
by interacting with the Zope sessioning machinery.
</div>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Id
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">This object's Zope id must be<br>
"browser_id_manager"
</div>
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Title
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="title" SIZE="40">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Browser Id Name
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="idname" SIZE="20" value="_ZopeId">
</TD>
</TR>
<tr>
<td>&nbsp;</td>
</tr>
<tr>
<td>
<div align=left class="form-label">Look for Browser Id in</th>
</td>
<td>
<table border=0>
<tr>
<td align=left>
<input type="checkbox" name="location:list" value="cookies" CHECKED> Cookies
</td>
</tr>
<tr>
<td align=left>
<input type="checkbox" name="location:list" value="form" CHECKED> Forms and Query Strings
</td>
</tr>
<tr>
<td align=left>
<input type="checkbox" name="location:list" value="url"> URLs
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td>&nbsp;</td>
</tr>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Automatically Encode Zope-Generated<br>URLs With A Browser Id
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="checkbox" NAME="auto_url_encoding" SIZE="20">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Cookie Path
</div>
<div class="form-help">
leave blank to provide no path info in the browser cookie
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="cookiepath" SIZE="20" value="/">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Cookie Domain
</div>
<div class="form-help">
leave blank to send cookies without domain<br>
info -- however, if cookie domain is not blank,<br>
it must contain at least two dots
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="cookiedomain" SIZE="20">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Cookie Lifetime In Days
</div>
<div class="form-help">
0 means send cookies which last only for the<br>
lifetime of the browser
</div>
</EM>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="cookielifedays:int" SIZE="20" value="0">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Only Send Cookie Over HTTPS
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="checkbox" NAME="cookiesecure">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Make cookie not aviable from JavaScript
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="checkbox" NAME="cookiehttponly">
</TD>
</TR>
</TR> <TR> <TD></TD> <TD>
<INPUT class="form-element" TYPE="SUBMIT" VALUE=" Add ">
</TD> </TR> </TABLE> </FORM>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var "manage_tabs(this(), _,
form_title='Manage Session Data Manager'
)">
<p class="form-help" colspan=2>
A Session Data Manager object is responsible for maintaining a
relationship between session data objects and Zope browser ids.
It is part of the Zope sessioning machinery. Programmers may
interact with a session data manager in order to obtain
information about session data, but will more often use the
REQUEST.SESSION object to do sessioning-related tasks.
</p>
<form action="manage_changeSDM" method="post">
<table cellspacing="2">
<tr>
<td align="left" valign="top">
<div class="form-label">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size="60" value="&dtml-title;">
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Transient Object Container Path
</div>
<div class="form-help">
e.g. '/temp_folder/session_data'
</div>
</td>
<td align="left" valign="top">
<input type="text" name="path" size="60"
value="&dtml-getContainerPath;">
</td>
</tr>
<tr>
<td align="LEFT" valign="TOP">
<div class="form-label">
Place SESSION in REQUEST object as
</div>
</td>
<td align="LEFT" valign="TOP">
<input class="form-element" type="TEXT" name="requestName"
value="&dtml-getRequestName;">
</td>
</tr>
<tr>
<td>
</td>
<td align="left" valign="top">
<div class="form-element">
<input class="form-element" type="submit" value = " Change ">
</div>
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var "manage_tabs(this(), _,
form_title='Manage Browser Id Manager'
)">
<FORM ACTION="manage_changeBrowserIdManager" METHOD="POST">
<p class="form-help">
Zope Browser Id Manager objects allow Zope to differentiate between site
visitors by "tagging" each of their browsers with a unique identifier. This
is useful if you need to tell visitors apart from one another even if they do
not "log in" to your site. Browser Id Managers are generally used
by interacting with the Zope sessioning machinery.
</p>
<TABLE CELLSPACING="2" border="0">
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Title
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="title" SIZE="30" value="&dtml-title;">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Browser Id Name
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="idname" SIZE="20" value="&dtml-getBrowserIdName;">
</TD>
</TR>
<dtml-let namespaces=getBrowserIdNamespaces>
<tr valign="top">
<td>
<div align=left class="form-label">Look for Browser Id in</th>
</td>
<td>
<table border=0>
<tr>
<td align=left>
<input type="checkbox" name="location:list" value="cookies"
<dtml-if "'cookies' in namespaces">CHECKED</dtml-if>> Cookies
</td>
</tr>
<tr>
<td align=left>
<input type="checkbox" name="location:list" value="form"
<dtml-if "'form' in namespaces">CHECKED</dtml-if>> Forms and Query Strings
</td>
</tr>
<tr>
<td align=left>
<input type="checkbox" name="location:list" value="url"
<dtml-if "'url' in namespaces">CHECKED</dtml-if>> URLs
</td>
</tr>
</table>
</td>
</tr>
</dtml-let>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Automatically Encode Zope-Generated<br>URLs With A Browser Id
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="checkbox" NAME="auto_url_encoding"
<dtml-if getAutoUrlEncoding>CHECKED</dtml-if>>
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Cookie Path
</div>
<div class="form-help">
leave blank to provide no path info in the browser cookie
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="cookiepath" SIZE="20"
value="&dtml-getCookiePath;">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Cookie Domain
</div>
<div class="form-help">
leave blank to send cookies without domain <br>
info -- however, if cookie domain is not blank,<br>
it must contain at least two dots
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="cookiedomain" SIZE="20"
value="&dtml-getCookieDomain;">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Cookie Lifetime In Days
</div>
<div class="form-help">
0 means send cookies which last only for the<br>
lifetime of the browser
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="cookielifedays:int" SIZE="20"
value="&dtml-getCookieLifeDays;">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Only Send Cookie Over HTTPS
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="checkbox" NAME="cookiesecure"
<dtml-if getCookieSecure>CHECKED</dtml-if>>
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Make cookie not aviable from JavaScript
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="checkbox" NAME="cookiehttponly"
<dtml-if getCookieHTTPOnly>CHECKED</dtml-if>>
</TD>
</TR>
<TR>
<TD></TD>
<TD><BR><INPUT class="form-element" TYPE="SUBMIT" VALUE=" Change "></TD>
</TR>
</TABLE>
</FORM>
<dtml-var manage_page_footer>
############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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 APIs
o See Also
- "Transient Object API":../../Transience/TransienceInterfaces.py
"""
from zope.interface import Interface
class IBrowserIdManager(Interface):
""" Zope Browser Id Manager interface.
A Zope Browser Id Manager is responsible for assigning ids to site
visitors, and for servicing requests from Session Data Managers
related to the browser id.
"""
def hasBrowserId():
""" Return true if there is a browser id for the current request.
o Permission required: Access contents information
o Does *not* raise an error if the request contains a broken
browser id.
"""
def getBrowserId(create=1):
""" Return a browser id for the current request.
o If create is false, return None if there is no browser id associated
with the current request.
o If create is true, return a newly-created browser id if
there is no browser id associated with the current request.
o This method is useful in conjunction with 'getBrowserIdName' if you
wish to embed the browser-id-name/browser-id combination as a hidden
value in a POST-based form.
o The browser id is opaque, has no business meaning, and its length,
type, and composition are subject to change.
o Permission required: Access contents information
o Raises BrowserIdManagerErr if an ill-formed browser id
is found in REQUEST.
"""
def getBrowserIdName():
"""
Returns a string with the name of the cookie/form variable which is
used by the current browser id manager as the name to look up when
attempting to obtain the browser id value. For example, '_ZopeId'.
Permission required: Access contents information
"""
def isBrowserIdNew():
"""
Returns true if browser id is 'new'. A browser id is 'new'
when it is first created and the client has therefore not sent it
back to the server in any request.
Permission required: Access contents information
Raises: BrowserIdManagerErr. If there is no current browser id.
"""
def isBrowserIdFromCookie():
""" Return true if browser id comes from a cookie.
o Permission required: Access contents information
o Raise BrowserIdManagerErr if there is no current browser id.
"""
def isBrowserIdFromForm():
""" Return true if browser id comes from a form variable.
o Variable may come from either the query string or a post.
o Permission required: Access contents information
o Raise BrowserIdManagerErr if there is no current browser id.
"""
def isBrowserIdFromUrl():
""" Return true if browser id comes from a cookie.
o Permission required: Access contents information
o Raise BrowserIdManagerErr if there is no current browser id.
"""
def flushBrowserIdCookie():
""" Deletes the browser id cookie from the client browser.
o Permission required: Access contents information
o Raise BrowserIdManagerErr if the 'cookies' namespace isn't
a browser id namespace.
"""
def setBrowserIdCookieByForce(bid):
""" Sets the browser id cookie to browser id 'bid' by force.
o Useful when you need to 'chain' browser id cookies across domains
for the same user (perhaps temporarily using query strings).
o Permission required: Access contents information
o Raise BrowserIdManagerErr if the 'cookies' namespace isn't
a browser id namespace.
"""
def getHiddenFormField():
""" Return a string usable as a hidden form field for the browser id.
o String is of the form::
<input type="hidden" name="_ZopeId" value="H7HJGYUFGFyHKH*" />
o name and the value represent the current browser id
name and current browser id.
"""
def encodeUrl(url, style='querystring'):
""" Encode a given URL with the current browser id.
o Two forms of URL-encoding are supported: 'querystring' and 'inline'.
o 'querystring' is the default.
o If the 'querystring' form is used, the browser id name/value pair
are postfixed onto the URL as a query string.
o If the 'inline' form is used, the browser id name/value pair
are prefixed onto the URL as the first two path segment elements.
o For example:
- The call encodeUrl('http://foo.com/amethod', style='querystring')
might return 'http://foo.com/amethod?_ZopeId=as9dfu0adfu0ad'.
- The call encodeUrl('http://foo.com/amethod, style='inline')
might return 'http://foo.com/_ZopeId/as9dfu0adfu0ad/amethod'.
o Permission required: Access contents information
o Raise BrowserIdManagerErr if there is no current browser id.
"""
class BrowserIdManagerErr(ValueError):
""" Error raised during some browser id manager operations
o See IBrowserIdManager methods.
o This exception may be caught in PythonScripts. A successful
import of the exception for PythonScript use would need to be::
from Products.Sessions.interfaces import BrowserIdManagerErr
"""
class ISessionDataManager(Interface):
""" Zope Session Data Manager interface.
A Zope Session Data Manager is responsible for maintaining Session
Data Objects, and for servicing requests from application code
related to Session Data Objects. It also communicates with a Browser
Id Manager to provide information about browser ids.
"""
def getBrowserIdManager():
""" Return the nearest acquirable browser id manager.
o Raise SessionDataManagerErr if no browser id manager can be found.
o Permission required: Access session data
"""
def getSessionData(create=1):
""" Return a Session Data Object for the current browser id.
o If there is no current browser id, and create is true,
return a new Session Data Object.
o If there is no current browser id and create is false, returns None.
o Permission required: Access session data
"""
def hasSessionData():
""" Does a Session Data Object exist for the current browser id?
o Do not create a Session Data Object if one does not exist.
o Permission required: Access session data
"""
def getSessionDataByKey(key):
""" Return a Session Data Object associated with 'key'.
o If there is no Session Data Object associated with 'key',
return None.
o Permission required: Access arbitrary user session data
"""
class SessionDataManagerErr(ValueError):
""" Error raised during some session data manager operations
o See ISesseionDataManager.
o This exception may be caught in PythonScripts. A successful
import of the exception for PythonScript use would need to be::
from Products.Sessions.interfaces import SessionDataManagerErr
"""
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
import time
from Testing import makerequest
import ZODB # in order to get Persistence.Persistent working
import transaction
import Acquisition
from Products.Sessions.BrowserIdManager import BrowserIdManager, \
getNewBrowserId
from Products.Sessions.SessionDataManager import \
SessionDataManager
from Products.Transience.Transience import \
TransientObjectContainer
from Products.TemporaryFolder.TemporaryFolder import MountedTemporaryFolder
from ZODB.POSException import ConflictError, \
ReadConflictError, BTreesConflictError
from unittest import TestCase, TestSuite, makeSuite
import threading, random
from ZODB.DemoStorage import DemoStorage
from OFS.Application import Application
import traceback
from Products.Transience.tests import fauxtime
import Products.Transience.Transience
import Products.Transience.TransientObject
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
tf_name = 'temp_folder'
idmgr_name = 'browser_id_manager'
toc_name = 'temp_transient_container'
sdm_name = 'session_data_manager'
stuff = {}
def log_time():
"""Return a simple time string without spaces suitable for logging."""
return ("%4.4d-%2.2d-%2.2dT%2.2d:%2.2d:%2.2d"
% time.localtime()[:6])
def _getDB():
db = stuff.get('db')
if not db:
ds = DemoStorage()
db = ZODB.DB(ds)
conn = db.open()
root = conn.root()
app = Application()
root['Application']= app
_populate(app)
transaction.commit()
stuff['db'] = db
conn.close()
return db
def _delDB():
transaction.abort()
del stuff['db']
class Foo(Acquisition.Implicit): pass
def _populate(app):
bidmgr = BrowserIdManager(idmgr_name)
tf = MountedTemporaryFolder(tf_name, 'Temporary Folder')
toc = TransientObjectContainer(toc_name, title='Temporary '
'Transient Object Container', timeout_mins=1)
session_data_manager=SessionDataManager(id=sdm_name,
path='/'+tf_name+'/'+toc_name, title='Session Data Manager')
try: app._delObject(idmgr_name)
except AttributeError: pass
try: app._delObject(tf_name)
except AttributeError: pass
try: app._delObject(sdm_name)
except AttributeError: pass
app._setObject(idmgr_name, bidmgr)
app._setObject(sdm_name, session_data_manager)
app._setObject(tf_name, tf)
transaction.commit()
app.temp_folder._setObject(toc_name, toc)
transaction.commit()
class TestMultiThread(TestCase):
def testOverlappingBrowserIds(self):
token = getNewBrowserId()
self.go(token)
def testNonOverlappingBrowserIds(self):
token = None
self.go(token)
def go(self, token):
readers = []
writers = []
valuers = []
readiters = 3
writeiters = 3
valueiters = 3
numreaders = 2
numwriters = 4
numvaluers = 1
db = _getDB()
for i in range(numreaders):
thread = ReaderThread(db, readiters, token)
readers.append(thread)
for i in range(numvaluers):
thread = ValuesGetterThread(db, valueiters, token)
valuers.append(thread)
for i in range(numwriters):
thread = WriterThread(db, writeiters, token)
writers.append(thread)
for thread in readers:
thread.start()
time.sleep(0.1)
for thread in writers:
thread.start()
time.sleep(0.1)
for thread in valuers:
thread.start()
time.sleep(0.1)
active = threading.activeCount()
while active > 0:
active = threading.activeCount()-1
print 'waiting for %s threads' % active
print "readers: ", numActive(readers),
print "writers: ", numActive(writers),
print "valuers: ", numActive(valuers)
time.sleep(5)
def numActive(threads):
i = 0
for thread in threads:
if not thread.isFinished():
i+=1
return i
class BaseReaderWriter(threading.Thread):
def __init__(self, db, iters, token=None):
self.iters = iters
self.sdm_name = sdm_name
self.finished = 0
self.db = db
self.token = token
threading.Thread.__init__(self)
def run(self):
i = 0
try:
while 1:
self.conn = self.db.open()
self.app = self.conn.root()['Application']
self.app = makerequest.makerequest(self.app)
if self.token is None:
token = getNewBrowserId()
else:
token = self.token
self.app.REQUEST.browser_id_ = token
try:
self.run1()
return
except ReadConflictError:
#traceback.print_exc()
print "R",
except BTreesConflictError:
print "B",
except ConflictError:
print "W",
except:
transaction.abort()
print log_time()
traceback.print_exc()
raise
i = i + 1
transaction.abort()
self.conn.close()
time.sleep(random.randrange(10) * .1)
finally:
transaction.abort()
self.conn.close()
del self.app
self.finished = 1
print '%s finished' % self.__class__
def isFinished(self):
return self.finished
class ReaderThread(BaseReaderWriter):
def run1(self):
session_data_manager = getattr(self.app, self.sdm_name)
data = session_data_manager.getSessionData(create=1)
t = time.time()
data[t] = 1
transaction.commit()
for i in range(self.iters):
data = session_data_manager.getSessionData()
time.sleep(random.choice(range(3)))
transaction.commit()
class WriterThread(BaseReaderWriter):
def run1(self):
session_data_manager = getattr(self.app, self.sdm_name)
for i in range(self.iters):
data = session_data_manager.getSessionData()
data[time.time()] = 1
n = random.choice(range(3))
time.sleep(n)
if n % 2 == 0:
transaction.commit()
else:
transaction.abort()
class ValuesGetterThread(BaseReaderWriter):
def run1(self):
tf = getattr(self.app, tf_name)
toc = getattr(tf, toc_name)
for i in range(self.iters):
print '%s values in toc' % len(toc.values())
n = random.choice(range(3))
time.sleep(n)
if n % 2 == 0:
transaction.commit()
else:
transaction.abort()
def test_suite():
test_multithread = makeSuite(TestMultiThread, 'test')
suite = TestSuite((test_multithread,))
return suite
##############################################################################
#
# Copyright (c) 2003 Zope Foundation 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.
#
##############################################################################
# This file is needed to make this a package.
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
"""
Test suite for session id manager.
"""
import unittest
import Testing.ZopeTestCase
class TestBrowserIdManager(unittest.TestCase):
def _getTargetClass(self):
from Products.Sessions.BrowserIdManager import BrowserIdManager
return BrowserIdManager
def _makeOne(self, request=None, name='browser_id_manager'):
bid = self._getTargetClass()(name)
if request is not None:
bid.REQUEST = request
return bid
def test_hasBrowserId_already_set_on_request_invalid(self):
request = DummyRequest(browser_id_='xxx')
mgr = self._makeOne(request)
self.assertFalse(mgr.hasBrowserId())
def test_hasBrowserId_already_set_on_request(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
request = DummyRequest(browser_id_=getNewBrowserId())
mgr = self._makeOne(request)
self.assertTrue(mgr.hasBrowserId())
def test_hasBrowserId_namespace_hit(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
request = DummyRequest(cookies={'bid': getNewBrowserId()})
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
self.assertTrue(mgr.hasBrowserId())
def test_hasBrowserId_namespace_miss(self):
request = DummyRequest()
mgr = self._makeOne(request)
self.assertFalse(mgr.hasBrowserId())
self.assertRaises(AttributeError, getattr, request, 'browser_id_')
self.assertRaises(AttributeError, getattr, request, 'browser_id_ns_')
def test_getBrowserId_already_set_on_request_invalid_raises(self):
request = DummyRequest(browser_id_='xxx')
mgr = self._makeOne(request)
self.assertRaises(ValueError, mgr.getBrowserId)
def test_getBrowserId_already_set_on_request(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid)
mgr = self._makeOne(request)
self.assertEqual(mgr.getBrowserId(), bid)
def test_getBrowserId_namespace_hit(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(cookies={'bid': bid})
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
self.assertTrue(mgr.hasBrowserId())
self.assertEqual(request.browser_id_, bid)
self.assertEqual(request.browser_id_ns_, 'cookies')
def test_getBrowserId_namespace_miss_no_create(self):
request = DummyRequest()
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
self.assertEqual(mgr.getBrowserId(create=False), None)
self.assertRaises(AttributeError, getattr, request, 'browser_id_')
self.assertRaises(AttributeError, getattr, request, 'browser_id_ns_')
def test_getBrowserId_namespace_miss_w_create_no_cookies(self):
from Products.Sessions.BrowserIdManager import isAWellFormedBrowserId
request = DummyRequest()
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setBrowserIdNamespaces(())
bid = mgr.getBrowserId()
self.assertTrue(isAWellFormedBrowserId(bid))
self.assertEqual(request.browser_id_, bid)
self.assertEqual(request.browser_id_ns_, None)
def test_getBrowserId_namespace_miss_w_create_w_cookies(self):
from Products.Sessions.BrowserIdManager import isAWellFormedBrowserId
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setBrowserIdNamespaces(('cookies',))
bid = mgr.getBrowserId()
self.assertTrue(isAWellFormedBrowserId(bid))
self.assertEqual(request.browser_id_, bid)
self.assertEqual(request.browser_id_ns_, None)
self.assertEqual(response.cookies['bid'], {'path': '/', 'value': bid})
def test_isBrowserIdNew_nonesuch_raises(self):
request = DummyRequest()
mgr = self._makeOne(request)
self.assertRaises(ValueError, mgr.isBrowserIdNew)
def test_isBrowserIdNew_no_ns(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid, browser_id_ns_=None)
mgr = self._makeOne(request)
self.assertTrue(mgr.isBrowserIdNew())
def test_isBrowserIdNew_w_ns(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid, browser_id_ns_='url')
mgr = self._makeOne(request)
self.assertFalse(mgr.isBrowserIdNew())
def test_isBrowserIdFromCookie_nonesuch_raises(self):
request = DummyRequest()
mgr = self._makeOne(request)
self.assertRaises(ValueError, mgr.isBrowserIdFromCookie)
def test_isBrowserIdFromCookie_wrong_ns(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid, browser_id_ns_='url')
mgr = self._makeOne(request)
self.assertFalse(mgr.isBrowserIdFromCookie())
def test_isBrowserIdFromCookie_right_ns(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid, browser_id_ns_='cookies')
mgr = self._makeOne(request)
self.assertTrue(mgr.isBrowserIdFromCookie())
def test_isBrowserIdFromForm_nonesuch_raises(self):
request = DummyRequest()
mgr = self._makeOne(request)
self.assertRaises(ValueError, mgr.isBrowserIdFromForm)
def test_isBrowserIdFromForm_wrong_ns(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid, browser_id_ns_='url')
mgr = self._makeOne(request)
self.assertFalse(mgr.isBrowserIdFromForm())
def test_isBrowserIdFromForm_right_ns(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid, browser_id_ns_='form')
mgr = self._makeOne(request)
self.assertTrue(mgr.isBrowserIdFromForm())
def test_isBrowserIdFromUrl_nonesuch_raises(self):
request = DummyRequest()
mgr = self._makeOne(request)
self.assertRaises(ValueError, mgr.isBrowserIdFromUrl)
def test_isBrowserIdFromUrl_wrong_ns(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid, browser_id_ns_='form')
mgr = self._makeOne(request)
self.assertFalse(mgr.isBrowserIdFromUrl())
def test_isBrowserIdFromUrl_right_ns(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid, browser_id_ns_='url')
mgr = self._makeOne(request)
self.assertTrue(mgr.isBrowserIdFromUrl())
def test_flushBrowserIdCookie_wrong_ns_raises(self):
mgr = self._makeOne()
mgr.setBrowserIdNamespaces(('url', 'form'))
self.assertRaises(ValueError, mgr.flushBrowserIdCookie)
def test_flushBrowserIdCookie_ok(self):
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setBrowserIdNamespaces(('cookies',))
mgr.flushBrowserIdCookie()
self.assertEqual(response.cookies['bid'],
{'path': '/',
'expires': 'Sun, 10-May-1971 11:59:00 GMT',
'value': 'deleted'})
def test_setBrowserIdCookieByForce_wrong_ns_raises(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
mgr = self._makeOne()
mgr.setBrowserIdNamespaces(('url', 'form'))
self.assertRaises(ValueError, mgr.setBrowserIdCookieByForce, bid)
def test_setBrowserIdCookieByForce_ok(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setBrowserIdNamespaces(('cookies',))
mgr.setBrowserIdCookieByForce(bid)
self.assertEqual(response.cookies['bid'], {'path': '/', 'value': bid})
def test_getHiddenFormField(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
self.assertEqual(mgr.getHiddenFormField(),
'<input type="hidden" name="bid" value="%s" />' % bid)
def test_encodeUrl_no_create_no_bid_raises(self):
URL = 'http://example.com/'
request = DummyRequest()
mgr = self._makeOne(request)
self.assertRaises(ValueError, mgr.encodeUrl, URL, create=False)
def test_encodeUrl_no_create_w_bid_querystring_style(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
URL = 'http://example.com/'
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
munged = mgr.encodeUrl(URL, create=False)
self.assertEqual(munged, '%s?bid=%s' % (URL, bid))
def test_encodeUrl_no_create_w_bid_querystring_style_existing_qs(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
URL = 'http://example.com/'
QS = 'foo=bar'
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
munged = mgr.encodeUrl('%s?%s' % (URL, QS), create=False)
self.assertEqual(munged, '%s?%s&amp;bid=%s' % (URL, QS, bid))
def test_encodeUrl_no_create_w_bid_inline_style(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
NETHOST = 'http://example.com'
PATH_INFO = 'path/to/page'
URL = '%s/%s' % (NETHOST, PATH_INFO)
bid = getNewBrowserId()
request = DummyRequest(browser_id_=bid)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
munged = mgr.encodeUrl(URL, style='inline', create=False)
self.assertEqual(munged, '%s/bid/%s/%s' % (NETHOST, bid, PATH_INFO))
def test_setBrowserIdName_empty_string_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError, mgr.setBrowserIdName, '')
def test_setBrowserIdName_non_string_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError, mgr.setBrowserIdName, 1)
def test_setBrowserIdName_normal(self):
mgr = self._makeOne()
mgr.setBrowserIdName('foo')
self.assertEqual(mgr.getBrowserIdName(), 'foo')
def test_setBrowserIdNamespaces_invalid_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError,
mgr.setBrowserIdNamespaces, ('gummy', 'froopy'))
def test_setBrowserIdNamespaces_normal(self):
NAMESPACES = ('cookies', 'url', 'form')
mgr = self._makeOne()
mgr.setBrowserIdNamespaces(NAMESPACES)
self.assertEqual(mgr.getBrowserIdNamespaces(), NAMESPACES)
def test_setCookiePath_invalid_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError, mgr.setCookiePath, '/;')
def test_setCookiePath_normal(self):
mgr = self._makeOne()
mgr.setCookiePath('/foo')
self.assertEqual(mgr.getCookiePath(), '/foo')
def test_setCookieLifeDays_invalid_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError, mgr.setCookieLifeDays, '')
def test_setCookieLifeDays_normal(self):
mgr = self._makeOne()
mgr.setCookieLifeDays(1)
self.assertEqual(mgr.getCookieLifeDays(), 1)
def test_setCookieDomain_non_string_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError, mgr.setCookieDomain, {1:1})
def test_setCookieDomain_no_dots_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError, mgr.setCookieDomain, 'gubble')
def test_setCookieDomain_one_dot_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError, mgr.setCookieDomain, 'zope.org')
def test_setCookieDomain_trailing_semicolon_raises(self):
mgr = self._makeOne()
self.assertRaises(ValueError, mgr.setCookieDomain, '.zope.org;')
def test_setCookieDomain_empty_OK(self):
mgr = self._makeOne()
mgr.setCookieDomain('')
self.assertEqual(mgr.getCookieDomain(), '')
def test_setCookieDomain_two_dots(self):
mgr = self._makeOne()
mgr.setCookieDomain('.zope.org')
self.assertEqual(mgr.getCookieDomain(), '.zope.org')
def test_setCookieDomain_three_dots(self):
mgr = self._makeOne()
mgr.setCookieDomain('.dev.zope.org')
self.assertEqual(mgr.getCookieDomain(), '.dev.zope.org')
def test_setCookieSecure_int(self):
mgr = self._makeOne()
mgr.setCookieSecure(1)
self.assertTrue(mgr.getCookieSecure())
mgr.setCookieSecure(0)
self.assertFalse(mgr.getCookieSecure())
def test_setCookieSecure_bool(self):
mgr = self._makeOne()
mgr.setCookieSecure(True)
self.assertTrue(mgr.getCookieSecure())
mgr.setCookieSecure(False)
self.assertFalse(mgr.getCookieSecure())
def test_setCookieHTTPOnly_bool(self):
mgr = self._makeOne()
mgr.setCookieHTTPOnly(True)
self.assertTrue(mgr.getCookieHTTPOnly())
mgr.setCookieHTTPOnly(False)
self.assertFalse(mgr.getCookieHTTPOnly())
def test_setAutoUrlEncoding_bool(self):
mgr = self._makeOne()
mgr.setAutoUrlEncoding(True)
self.assertTrue(mgr.getAutoUrlEncoding())
mgr.setAutoUrlEncoding(False)
self.assertFalse(mgr.getAutoUrlEncoding())
def test_isUrlInBidNamespaces(self):
mgr = self._makeOne()
mgr.setBrowserIdNamespaces(('cookies', 'url', 'form'))
self.assertTrue(mgr.isUrlInBidNamespaces())
mgr.setBrowserIdNamespaces(('cookies', 'form'))
self.assertFalse(mgr.isUrlInBidNamespaces())
def test__setCookie_remove(self):
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr._setCookie('xxx', request, remove=True)
self.assertEqual(response.cookies['bid'],
{'path': '/', 'value': 'xxx',
'expires': 'Sun, 10-May-1971 11:59:00 GMT'})
def test__setCookie_cookie_life_days(self):
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setCookieLifeDays(1)
mgr._setCookie('xxx', request,
now=lambda: 1,
strftime=lambda x, y: 'Seconds: %d' % y,
gmtime=lambda x: x)
self.assertEqual(response.cookies['bid'],
{'path': '/', 'value': 'xxx',
'expires': 'Seconds: 86401'})
def test__setCookie_cookie_secure_no_URL1_sets_no_cookie(self):
request = DummyRequest()
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setCookieSecure(True)
mgr._setCookie('xxx', request) # no response, doesn't blow up
def test__setCookie_cookie_secure_not_https_sets_no_cookie(self):
request = DummyRequest(URL1='http://example.com/')
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setCookieSecure(True)
mgr._setCookie('xxx', request) # no response, doesn't blow up
def test__setCookie_cookie_secure_is_https(self):
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response, URL1='https://example.com/')
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setCookieSecure(True)
mgr._setCookie('xxx', request)
self.assertEqual(response.cookies['bid'],
{'path': '/', 'value': 'xxx', 'secure': True})
def test__setCookie_domain(self):
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setCookieDomain('.zope.org')
mgr._setCookie('xxx', request)
self.assertEqual(response.cookies['bid'],
{'path': '/', 'value': 'xxx', 'domain': '.zope.org'})
def test__setCookie_path(self):
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response)
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setCookiePath('/path/')
mgr._setCookie('xxx', request)
self.assertEqual(response.cookies['bid'],
{'path': '/path/', 'value': 'xxx'})
def test__setCookie_http_only(self):
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response, URL1='https://example.com/')
mgr = self._makeOne(request)
mgr.setBrowserIdName('bid')
mgr.setCookieHTTPOnly(True)
mgr._setCookie('xxx', request)
self.assertEqual(response.cookies['bid'],
{'path': '/', 'value': 'xxx', 'http_only': True})
def test__setCookie_http_only_missing_attr(self):
# See https://bugs.launchpad.net/bugs/374816
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response, URL1='https://example.com/')
mgr = self._makeOne(request)
del mgr.cookie_http_only # pre-2.12 instances didn't have this
mgr.setBrowserIdName('bid')
mgr._setCookie('xxx', request)
self.assertEqual(response.cookies['bid'],
{'path': '/', 'value': 'xxx'})
def test__setId_same_id_noop(self):
mgr = self._makeOne(name='foo')
mgr._setId('foo')
def test__setId_different_id_raises(self):
mgr = self._makeOne(name='foo')
self.assertRaises(ValueError, mgr._setId, 'bar')
def test_setCookieSecure_non_HTTPS_doesnt_set_cookie(self):
# Document the "feature" that 'setCookieSecure' allows returning
# a browser ID even where the URL is not HTTPS, and therefor no
# cookie is set.
response = DummyResponse(cookies={})
request = DummyRequest(RESPONSE=response, URL1='http://example.com/')
mgr = self._makeOne(request)
mgr.setCookieSecure(1)
bid = mgr.getBrowserId() # doesn't raise
self.assertEqual(len(response.cookies), 0)
def test_hasTraversalHook_missing(self):
mgr = self._makeOne()
parent = DummyObject()
self.assertFalse(mgr.hasTraversalHook(parent))
def test_hasTraversalHook_present(self):
mgr = self._makeOne()
parent = DummyObject()
parent.__before_traverse__ = {(0, 'BrowserIdManager'): object()}
self.assertTrue(mgr.hasTraversalHook(parent))
def test_updateTraversalData_w_url_ns(self):
from Acquisition import Implicit
from ZPublisher.BeforeTraverse import queryBeforeTraverse
from Products.Sessions.BrowserIdManager import BrowserIdManagerTraverser
class Parent(Implicit):
pass
mgr = self._makeOne()
mgr.setBrowserIdNamespaces(('url',))
parent = Parent()
parent.browser_id_manager = mgr
parent.browser_id_manager.updateTraversalData() # needs wrapper
hooks = queryBeforeTraverse(parent, 'BrowserIdManager')
self.assertEqual(len(hooks), 1)
self.assertEqual(hooks[0][0], 40)
self.assertTrue(isinstance(hooks[0][1], BrowserIdManagerTraverser))
def test_updateTraversalData_not_url_ns(self):
from Acquisition import Implicit
from ZPublisher.BeforeTraverse import queryBeforeTraverse
class Parent(Implicit):
pass
mgr = self._makeOne()
mgr.setBrowserIdNamespaces(('cookies', 'form'))
parent = Parent()
parent.__before_traverse__ = {(0, 'BrowserIdManager'): object()}
parent.browser_id_manager = mgr
parent.browser_id_manager.updateTraversalData() # needs wrapper
self.assertFalse(queryBeforeTraverse(mgr, 'BrowserIdManager'))
def test_registerTraversalHook_doesnt_replace_existing(self):
from Acquisition import Implicit
from ZPublisher.BeforeTraverse import queryBeforeTraverse
class Parent(Implicit):
pass
mgr = self._makeOne()
parent = Parent()
hook = object()
parent.__before_traverse__ = {(0, 'BrowserIdManager'): hook}
parent.browser_id_manager = mgr
parent.browser_id_manager.registerTraversalHook() # needs wrapper
hooks = queryBeforeTraverse(parent, 'BrowserIdManager')
self.assertEqual(len(hooks), 1)
self.assertEqual(hooks[0][0], 0)
self.assertTrue(hooks[0][1] is hook)
def test_registerTraversalHook_normal(self):
from Acquisition import Implicit
from ZPublisher.BeforeTraverse import queryBeforeTraverse
from Products.Sessions.BrowserIdManager import BrowserIdManagerTraverser
class Parent(Implicit):
pass
mgr = self._makeOne()
parent = Parent()
parent.browser_id_manager = mgr
parent.browser_id_manager.registerTraversalHook() # needs wrapper
hooks = queryBeforeTraverse(parent, 'BrowserIdManager')
self.assertEqual(len(hooks), 1)
self.assertEqual(hooks[0][0], 40)
self.assertTrue(isinstance(hooks[0][1], BrowserIdManagerTraverser))
def test_unregisterTraversalHook_nonesuch_doesnt_raise(self):
from Acquisition import Implicit
class Parent(Implicit):
pass
mgr = self._makeOne()
parent = Parent()
parent.browser_id_manager = mgr
parent.browser_id_manager.unregisterTraversalHook() # needs wrapper
def test_unregisterTraversalHook_normal(self):
from Acquisition import Implicit
from ZPublisher.BeforeTraverse import queryBeforeTraverse
class Parent(Implicit):
pass
mgr = self._makeOne()
parent = Parent()
parent.__before_traverse__ = {(0, 'BrowserIdManager'): object()}
parent.browser_id_manager = mgr
parent.browser_id_manager.unregisterTraversalHook() # needs wrapper
self.assertFalse(queryBeforeTraverse(mgr, 'BrowserIdManager'))
class TestBrowserIdManagerTraverser(unittest.TestCase):
def _getTargetClass(self):
from Products.Sessions.BrowserIdManager \
import BrowserIdManagerTraverser
return BrowserIdManagerTraverser
def _makeOne(self):
return self._getTargetClass()()
def test___call___no_mgr(self):
traverser = self._makeOne()
container = DummyObject()
request = DummyRequest()
traverser(container, request) # doesn't raise
def test___call___w_mgr_request_has_no_stack(self):
traverser = self._makeOne()
mgr = DummyBrowserIdManager()
container = DummyObject(browser_id_manager=mgr)
request = DummyRequest()
traverser(container, request) # doesn't raise
def test___call___w_mgr_request_has_stack_no_auto_encode(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
traverser = self._makeOne()
mgr = DummyBrowserIdManager()
container = DummyObject(browser_id_manager=mgr)
request = DummyRequest(
TraversalRequestNameStack=[bid, 'bid'])
traverser(container, request)
self.assertEqual(request.browser_id_, bid)
self.assertEqual(request.browser_id_ns_, 'url')
self.assertEqual(len(request.TraversalRequestNameStack), 0)
def test___call___w_mgr_request_has_stack_w_auto_encode(self):
from Products.Sessions.BrowserIdManager import getNewBrowserId
bid = getNewBrowserId()
traverser = self._makeOne()
mgr = DummyBrowserIdManager(True)
container = DummyObject(browser_id_manager=mgr)
request = DummyRequest(
TraversalRequestNameStack=[bid, 'bid'], _script=[])
traverser(container, request)
self.assertEqual(request.browser_id_, bid)
self.assertEqual(request.browser_id_ns_, 'url')
self.assertEqual(len(request.TraversalRequestNameStack), 0)
self.assertEqual(len(request._script), 2)
self.assertEqual(request._script[0], 'bid')
self.assertEqual(request._script[1], bid)
def test___call___w_mgr_request_empty_stack_w_auto_encode(self):
from Products.Sessions.BrowserIdManager import isAWellFormedBrowserId
traverser = self._makeOne()
mgr = DummyBrowserIdManager(True)
container = DummyObject(browser_id_manager=mgr)
request = DummyRequest( TraversalRequestNameStack=[], _script=[])
traverser(container, request)
bid = request.browser_id_
self.assertTrue(isAWellFormedBrowserId(bid))
self.assertEqual(request.browser_id_ns_, None)
self.assertEqual(len(request.TraversalRequestNameStack), 0)
self.assertEqual(len(request._script), 2)
self.assertEqual(request._script[0], 'bid')
self.assertEqual(request._script[1], bid)
class TestBrowserIdManagerPublish(Testing.ZopeTestCase.FunctionalTestCase):
def test_encodeUrl_safe(self):
from Products.Sessions.BrowserIdManager import BrowserIdManager
if not hasattr(self.app, 'browser_id_manager'):
bid = BrowserIdManager('browser_id_manager', 'Browser Id Manager')
self.app._setObject('browser_id_manager', bid)
res = self.publish(
'/browser_id_manager/encodeUrl?url=%3Chtml%3EEVIL%2Fhtml%3E%3C!--')
self.assertFalse("<html>EVIL/html>" in res.getBody())
class DummyObject:
def __init__(self, **kw):
self.__dict__.update(kw)
class DummyResponse(DummyObject):
pass
class DummyRequest(DummyObject):
def __getitem__(self, key):
return getattr(self, key)
def get(self, key, default=None):
return getattr(self, key, default)
class DummyBrowserIdManager:
def __init__(self, auto=False):
self._auto = auto
def getBrowserIdName(self):
return 'bid'
def getAutoUrlEncoding(self):
return self._auto
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TestBrowserIdManager),
unittest.makeSuite(TestBrowserIdManagerTraverser),
unittest.makeSuite(TestBrowserIdManagerPublish),
))
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
import unittest
tf_name = 'temp_folder'
idmgr_name = 'browser_id_manager'
toc_name = 'temp_transient_container'
sdm_name = 'session_data_manager'
stuff = {}
def _getDB():
from OFS.Application import Application
import transaction
db = stuff.get('db')
if not db:
from ZODB import DB
from ZODB.DemoStorage import DemoStorage
ds = DemoStorage()
db = DB(ds, pool_size=60)
conn = db.open()
root = conn.root()
app = Application()
root['Application']= app
transaction.savepoint(optimistic=True)
_populate(app)
stuff['db'] = db
conn.close()
return db
def _delDB():
import transaction
transaction.abort()
del stuff['db']
def _populate(app):
from OFS.DTMLMethod import DTMLMethod
from Products.Sessions.BrowserIdManager import BrowserIdManager
from Products.Sessions.SessionDataManager import SessionDataManager
from Products.TemporaryFolder.TemporaryFolder import MountedTemporaryFolder
from Products.Transience.Transience import TransientObjectContainer
import transaction
bidmgr = BrowserIdManager(idmgr_name)
tf = MountedTemporaryFolder(tf_name, title="Temporary Folder")
toc = TransientObjectContainer(toc_name, title='Temporary '
'Transient Object Container', timeout_mins=20)
session_data_manager=SessionDataManager(id=sdm_name,
path='/'+tf_name+'/'+toc_name, title='Session Data Manager',
requestName='TESTOFSESSION')
try:
app._delObject(idmgr_name)
except (AttributeError, KeyError):
pass
try:
app._delObject(tf_name)
except (AttributeError, KeyError):
pass
try:
app._delObject(sdm_name)
except (AttributeError, KeyError):
pass
try:
app._delObject('index_html')
except (AttributeError, KeyError):
pass
app._setObject(idmgr_name, bidmgr)
app._setObject(sdm_name, session_data_manager)
app._setObject(tf_name, tf)
transaction.commit()
app.temp_folder._setObject(toc_name, toc)
transaction.commit()
# index_html necessary for publishing emulation for testAutoReqPopulate
app._setObject('index_html', DTMLMethod('', __name__='foo'))
transaction.commit()
class TestSessionManager(unittest.TestCase):
def setUp(self):
from Testing import makerequest
db = _getDB()
conn = db.open()
root = conn.root()
self.app = makerequest.makerequest(root['Application'])
timeout = self.timeout = 1
def tearDown(self):
_delDB()
self.app._p_jar.close()
del self.app
def testHasId(self):
self.assertTrue(self.app.session_data_manager.id == \
sdm_name)
def testHasTitle(self):
self.assertTrue(self.app.session_data_manager.title \
== 'Session Data Manager')
def testGetSessionDataNoCreate(self):
sd = self.app.session_data_manager.getSessionData(0)
self.assertTrue(sd is None)
def testGetSessionDataCreate(self):
from Products.Transience.Transience import TransientObject
sd = self.app.session_data_manager.getSessionData(1)
self.assertTrue(sd.__class__ is TransientObject)
def testHasSessionData(self):
sd = self.app.session_data_manager.getSessionData()
self.assertTrue(self.app.session_data_manager.hasSessionData())
def testNotHasSessionData(self):
self.assertTrue(not self.app.session_data_manager.hasSessionData())
def testSessionDataWrappedInSDMandTOC(self):
from Acquisition import aq_base
sd = self.app.session_data_manager.getSessionData(1)
sdm = aq_base(getattr(self.app, sdm_name))
toc = aq_base(getattr(self.app.temp_folder, toc_name))
self.assertTrue(aq_base(sd.aq_parent) is sdm)
self.assertTrue(aq_base(sd.aq_parent.aq_parent) is toc)
def testNewSessionDataObjectIsValid(self):
from Acquisition import aq_base
from Products.Transience.Transience import TransientObject
sdType = type(TransientObject(1))
sd = self.app.session_data_manager.getSessionData()
self.assertTrue(type(aq_base(sd)) is sdType)
self.assertTrue(not hasattr(sd, '_invalid'))
def testBrowserIdIsSet(self):
sd = self.app.session_data_manager.getSessionData()
mgr = getattr(self.app, idmgr_name)
self.assertTrue(mgr.hasBrowserId())
def testGetSessionDataByKey(self):
sd = self.app.session_data_manager.getSessionData()
mgr = getattr(self.app, idmgr_name)
token = mgr.getBrowserId()
bykeysd = self.app.session_data_manager.getSessionDataByKey(token)
self.assertTrue(sd == bykeysd)
def testBadExternalSDCPath(self):
from Products.Sessions.SessionDataManager import SessionDataManagerErr
sdm = self.app.session_data_manager
# fake out webdav
sdm.REQUEST['REQUEST_METHOD'] = 'GET'
sdm.setContainerPath('/fudgeffoloo')
self.assertRaises(SessionDataManagerErr, self._testbadsdcpath)
def _testbadsdcpath(self):
self.app.session_data_manager.getSessionData()
def testInvalidateSessionDataObject(self):
sdm = self.app.session_data_manager
sd = sdm.getSessionData()
sd['test'] = 'Its alive! Alive!'
sd.invalidate()
self.assertTrue(not sdm.getSessionData().has_key('test'))
def testGhostUnghostSessionManager(self):
import transaction
sdm = self.app.session_data_manager
transaction.commit()
sd = sdm.getSessionData()
sd.set('foo', 'bar')
sdm._p_changed = None
transaction.commit()
self.assertTrue(sdm.getSessionData().get('foo') == 'bar')
def testSubcommitAssignsPJar(self):
global DummyPersistent # so pickle can find it
from Persistence import Persistent
import transaction
class DummyPersistent(Persistent):
pass
sd = self.app.session_data_manager.getSessionData()
dummy = DummyPersistent()
sd.set('dp', dummy)
self.assertTrue(sd['dp']._p_jar is None)
transaction.savepoint(optimistic=True)
self.assertFalse(sd['dp']._p_jar is None)
def testForeignObject(self):
from ZODB.POSException import InvalidObjectReference
self.assertRaises(InvalidObjectReference, self._foreignAdd)
def _foreignAdd(self):
import transaction
ob = self.app.session_data_manager
# we don't want to fail due to an acquisition wrapper
ob = ob.aq_base
# we want to fail for some other reason:
sd = self.app.session_data_manager.getSessionData()
sd.set('foo', ob)
transaction.commit()
def testAqWrappedObjectsFail(self):
from Acquisition import Implicit
import transaction
class DummyAqImplicit(Implicit):
pass
a = DummyAqImplicit()
b = DummyAqImplicit()
aq_wrapped = a.__of__(b)
sd = self.app.session_data_manager.getSessionData()
sd.set('foo', aq_wrapped)
self.assertRaises(TypeError, transaction.commit)
def testAutoReqPopulate(self):
self.app.REQUEST['PARENTS'] = [self.app]
self.app.REQUEST['URL'] = 'a'
self.app.REQUEST.traverse('/')
self.assertTrue(self.app.REQUEST.has_key('TESTOFSESSION'))
def testUnlazifyAutoPopulated(self):
from Acquisition import aq_base
from Products.Transience.Transience import TransientObject
self.app.REQUEST['PARENTS'] = [self.app]
self.app.REQUEST['URL'] = 'a'
self.app.REQUEST.traverse('/')
sess = self.app.REQUEST['TESTOFSESSION']
sdType = type(TransientObject(1))
self.assertTrue(type(aq_base(sess)) is sdType)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(TestSessionManager),
))
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
"""
Module used for testing transience (BTree-API-conforming data structure)
"""
from Persistence.mapping import PersistentMapping
import sys
class FakeIOBTree(PersistentMapping):
def keys(self, min, max):
L = []
if min is None:
min = 0
if max is None:
max = sys.maxint
for k in self.data:
if min <= k <= max:
L.append(k)
return L
import time
class PreventTransactionCommit(Exception):
def __init__(self, reason):
self. reason = reason
def __str__(self):
return "Uncommittable transaction: " % self.reason
class UncommittableJar:
""" A jar that cannot be committed """
def __init__(self, reason):
self.reason = reason
self.time = time.time()
def sortKey(self):
return str(id(self))
def tpc_begin(self, *arg, **kw):
pass
def commit(self, obj, transaction):
pass
def tpc_vote(self, transaction):
raise PreventTransactionCommit(self.reason)
def abort(*args):
pass
class makeTransactionUncommittable:
"""
- register an uncommittable object with the provided transaction
which prevents the commit of that transaction
"""
def __init__(self, transaction, reason):
self._p_jar = UncommittableJar(reason)
transaction.register(self)
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
"""
Transient Object Container Class ('timeslice'-based design, no index).
"""
from cgi import escape
from logging import getLogger
import math
import os
import random
import sys
import thread
import time
from AccessControl.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo
from AccessControl.SecurityManagement import getSecurityManager
from AccessControl.SecurityManagement import newSecurityManager
from AccessControl.SecurityManagement import setSecurityManager
from AccessControl.SpecialUsers import nobody
from App.special_dtml import HTMLFile
from BTrees.Length import Length as BTreesLength
from BTrees.OOBTree import OOBTree
from BTrees.IOBTree import IOBTree
from OFS.SimpleItem import SimpleItem
from Persistence import Persistent
from zope.interface import implements
from Products.Transience.TransienceInterfaces import DictionaryLike
from Products.Transience.TransienceInterfaces \
import ImmutablyValuedMappingOfPickleableObjects
from Products.Transience.TransienceInterfaces import ItemWithId
from Products.Transience.TransienceInterfaces \
import StringKeyedHomogeneousItemContainer
from Products.Transience.TransienceInterfaces import Transient
from Products.Transience.TransienceInterfaces import TransientItemContainer
from Products.Transience.TransienceInterfaces import TTWDictionary
from Products.Transience.TransientObject import TransientObject
from Products.Transience.Fake import FakeIOBTree
ADD_CONTAINER_PERM = 'Add Transient Object Container'
MGMT_SCREEN_PERM = 'View management screens'
ACCESS_CONTENTS_PERM = 'Access contents information'
CREATE_TRANSIENTS_PERM = 'Create Transient Objects'
ACCESS_TRANSIENTS_PERM = 'Access Transient Objects'
MANAGE_CONTAINER_PERM = 'Manage Transient Object Container'
SPARE_BUCKETS = 15 # minimum number of buckets to keep "spare"
BUCKET_CLASS = OOBTree # constructor for buckets
DATA_CLASS = IOBTree # const for main data structure (timeslice->"bucket")
STRICT = os.environ.get('Z_TOC_STRICT', '')
DEBUG = int(os.environ.get('Z_TOC_DEBUG', 0))
_marker = []
LOG = getLogger('Transience')
def setStrict(on=''):
""" Turn on assertions (which may cause conflicts) """
global STRICT
STRICT = on
def TLOG(*args):
sargs = []
sargs.append(str(thread.get_ident()))
sargs.append(str(time.time()))
for arg in args:
sargs.append(str(arg))
msg = ' '.join(sargs)
LOG.info(msg)
constructTransientObjectContainerForm = HTMLFile(
'dtml/addTransientObjectContainer', globals())
def constructTransientObjectContainer(self, id, title='', timeout_mins=20,
addNotification=None, delNotification=None, limit=0, period_secs=20,
REQUEST=None):
""" """
ob = TransientObjectContainer(id, title, timeout_mins,
addNotification, delNotification, limit=limit, period_secs=period_secs)
self._setObject(id, ob)
if REQUEST is not None:
return self.manage_main(self, REQUEST, update_menu=1)
class MaxTransientObjectsExceeded(Exception): pass
class TransientObjectContainer(SimpleItem):
""" Object which contains items that are automatically flushed
after a period of inactivity """
meta_type = "Transient Object Container"
implements(ItemWithId,
StringKeyedHomogeneousItemContainer,
TransientItemContainer,
)
manage_options = (
{'label': 'Manage', 'action': 'manage_container'},
{'label': 'Security', 'action': 'manage_access'},
)
security = ClassSecurityInfo()
security.setPermissionDefault(MANAGE_CONTAINER_PERM,
['Manager',])
security.setPermissionDefault(MGMT_SCREEN_PERM,
['Manager',])
security.setPermissionDefault(ACCESS_CONTENTS_PERM,
['Manager','Anonymous'])
security.setPermissionDefault(ACCESS_TRANSIENTS_PERM,
['Manager','Anonymous','Sessions'])
security.setPermissionDefault(CREATE_TRANSIENTS_PERM,
['Manager',])
security.declareProtected(MGMT_SCREEN_PERM, 'manage_container')
manage_container = HTMLFile('dtml/manageTransientObjectContainer',
globals())
_limit = 0
_data = None
_inband_housekeeping = True
security.setDefaultAccess('deny')
# intitialize locks used for finalization, replentishing, and
# garbage collection (used in _finalize, _replentish, and _gc
# respectively)
finalize_lock = thread.allocate_lock()
replentish_lock = thread.allocate_lock()
gc_lock = thread.allocate_lock()
def __init__(self, id, title='', timeout_mins=20, addNotification=None,
delNotification=None, limit=0, period_secs=20):
self.id = id
self.title=title
self._setTimeout(timeout_mins, period_secs)
self._setLimit(limit)
self.setDelNotificationTarget(delNotification)
self.setAddNotificationTarget(addNotification)
self._reset()
# helpers
def _setTimeout(self, timeout_mins, period_secs):
if type(timeout_mins) is not type(1):
raise TypeError, (escape(`timeout_mins`), "Must be integer")
if type(period_secs) is not type(1):
raise TypeError, (escape(`period_secs`), "Must be integer")
timeout_secs = timeout_mins * 60
# special-case 0-minute timeout value by ignoring period
if timeout_secs != 0:
if period_secs == 0:
raise ValueError('resolution cannot be 0')
if period_secs > timeout_secs:
raise ValueError(
'resolution cannot be greater than timeout '
'minutes * 60 ( %s > %s )' % (period_secs, timeout_secs))
# we need the timeout to be evenly divisible by the period
if timeout_secs % period_secs != 0:
raise ValueError(
'timeout seconds (%s) must be evenly divisible '
'by resolution (%s)' % (timeout_secs, period_secs)
)
# our timeout secs is the number of seconds that an item should
# remain unexpired
self._timeout_secs = timeout_secs
# our _period is the number of seconds that constitutes a timeslice
self._period = period_secs
# timeout_slices == fewest number of timeslices that's >= timeout_secs
self._timeout_slices=int(math.ceil(float(timeout_secs)/period_secs))
def _setLimit(self, limit):
if type(limit) is not type(1):
raise TypeError, (escape(`limit`), "Must be integer")
self._limit = limit
def _reset(self):
""" Reset ourselves to a sane state (deletes all content) """
# _data contains a mapping of f-of-time(int) (aka "slice") to
# "bucket". Each bucket will contain a set of transient items.
# Transient items move automatically from bucket-to-bucket inside
# of the _data structure based on last access time (e.g.
# "get" calls), escaping expiration and eventual destruction only if
# they move quickly enough.
#
# We make enough buckets initially to last us a while, and
# we subsequently extend _data with fresh buckets and remove old
# buckets as necessary during normal operations (see
# _replentish() and _gc()).
self._data = DATA_CLASS()
# populate _data with some number of buckets, each of which
# is "current" for its timeslice key
if self._timeout_slices:
new_slices = getTimeslices(
getCurrentTimeslice(self._period),
SPARE_BUCKETS*2,
self._period)
for i in new_slices:
self._data[i] = BUCKET_CLASS()
# max_timeslice is at any time during operations the highest
# key value in _data. Its existence is an optimization; getting
# the maxKey of a BTree directly is read-conflict-prone.
self._max_timeslice = Increaser(max(new_slices))
else:
self._data[0] = BUCKET_CLASS() # sentinel value for non-expiring
self._max_timeslice = Increaser(0)
# '_last_finalized_timeslice' is a value that indicates which
# timeslice had its items last run through the finalization
# process. The finalization process calls the delete notifier for
# each expired item.
self._last_finalized_timeslice = Increaser(-self._period)
# '_last_gc_timeslice' is a value that indicates in which
# timeslice the garbage collection process was last run.
self._last_gc_timeslice = Increaser(-self._period)
# our "_length" is the number of "active" data objects in _data.
# it does not include items that are still kept in _data but need to
# be garbage collected.
#
# we need to maintain the length of the index structure separately
# because getting the length of a BTree is very expensive, and it
# doesn't really tell us which ones are "active" anyway.
try:
self._length.set(0)
except AttributeError:
self._length = self.getLen = Length2()
def _getCurrentSlices(self, now):
if self._timeout_slices:
begin = now - (self._period * self._timeout_slices)
# add add one to _timeout_slices below to account for the fact that
# a call to this method may happen any time within the current
# timeslice; calling it in the beginning of the timeslice can lead
# to sessions becoming invalid a maximum of self._period seconds
# earlier than the requested timeout value. Adding one here can
# lead to sessions becoming invalid *later* than the timeout value
# (also by a max of self._period), but in the common sessioning
# case, that seems preferable.
num_slices = self._timeout_slices + 1
else:
return [0] # sentinel for timeout value 0 (don't expire)
DEBUG and TLOG('_getCurrentSlices, now = %s ' % now)
DEBUG and TLOG('_getCurrentSlices, begin = %s' % begin)
DEBUG and TLOG('_getCurrentSlices, num_slices = %s' % num_slices)
result = getTimeslices(begin, num_slices, self._period)
DEBUG and TLOG('_getCurrentSlices, result = %s' % result)
return result
def _move_item(self, k, current_ts, default=None):
if not self._timeout_slices:
# special case for no timeout value
bucket = self._data.get(0)
return bucket.get(k, default)
if self._inband_housekeeping:
self._housekeep(current_ts)
else:
# dont allow the TOC to stop working in an emergency bucket
# shortage
if self._in_emergency_bucket_shortage(current_ts):
self._replentish(current_ts)
# SUBTLETY ALERTY TO SELF: do not "improve" the code below
# unnecessarily, as it will end only in tears. The lack of aliases
# and the ordering is intentional.
STRICT and _assert(self._data.has_key(current_ts))
current_slices = self._getCurrentSlices(current_ts)
found_ts = None
for ts in current_slices:
abucket = self._data.get(ts, None) # XXX ReadConflictError hotspot
if abucket is None:
DEBUG and TLOG('_move_item: no bucket for ts %s' % ts)
continue
DEBUG and TLOG(
'_move_item: bucket for ts %s is %s' % (ts, id(abucket)))
DEBUG and TLOG(
'_move_item: keys for ts %s (bucket %s)-- %s' %
(ts, id(abucket), str(list(abucket.keys())))
)
# uhghost?
if abucket.get(k, None) is not None:
found_ts = ts
break
DEBUG and TLOG('_move_item: found_ts is %s' % found_ts)
if found_ts is None:
DEBUG and TLOG('_move_item: returning default of %s' % default)
return default
if found_ts != current_ts:
DEBUG and TLOG('_move_item: current_ts (%s) != found_ts (%s), '
'moving to current' % (current_ts, found_ts))
DEBUG and TLOG(
'_move_item: keys for found_ts %s (bucket %s): %s' % (
found_ts, id(self._data[found_ts]),
`list(self._data[found_ts].keys())`)
)
self._data[current_ts][k] = self._data[found_ts][k]
if not issubclass(BUCKET_CLASS, Persistent):
# tickle persistence machinery
self._data[current_ts] = self._data[current_ts]
DEBUG and TLOG(
'_move_item: copied item %s from %s to %s (bucket %s)' % (
k, found_ts, current_ts, id(self._data[current_ts])))
del self._data[found_ts][k]
if not issubclass(BUCKET_CLASS, Persistent):
# tickle persistence machinery
self._data[found_ts] = self._data[found_ts]
DEBUG and TLOG(
'_move_item: deleted item %s from ts %s (bucket %s)' % (
k, found_ts, id(self._data[found_ts]))
)
STRICT and _assert(self._data[found_ts].get(k, None) is None)
STRICT and _assert(not self._data[found_ts].has_key(k))
if getattr(self._data[current_ts][k], 'setLastAccessed', None):
self._data[current_ts][k].setLastAccessed()
DEBUG and TLOG('_move_item: returning %s from current_ts %s '
% (k, current_ts))
return self._data[current_ts][k]
def _all(self):
if self._timeout_slices:
current_ts = getCurrentTimeslice(self._period)
else:
current_ts = 0
if self._inband_housekeeping:
self._housekeep(current_ts)
elif self._in_emergency_bucket_shortage(current_ts):
# if our scheduler fails, dont allow the TOC to stop working
self._replentish(current_ts, force=True)
STRICT and _assert(self._data.has_key(current_ts))
current = self._getCurrentSlices(current_ts)
current.reverse() # overwrite older with newer
d = {}
for ts in current:
bucket = self._data.get(ts)
if bucket is None:
continue
for k,v in bucket.items():
d[k] = self._wrap(v)
return d
def keys(self):
return self._all().keys()
def raw(self, current_ts):
# for debugging and unit testing
current = self._getCurrentSlices(current_ts)
current.reverse() # overwrite older with newer
d = {}
for ts in current:
bucket = self._data.get(ts, None)
if bucket is None:
continue
for k,v in bucket.items():
d[k] = self._wrap(v)
return d
def items(self):
return self._all().items()
def values(self):
return self._all().values()
def _wrap(self, item):
# dont use hasattr here (it hides conflict errors)
if getattr(item, '__of__', None):
item = item.__of__(self)
return item
def __getitem__(self, k):
if self._timeout_slices:
current_ts = getCurrentTimeslice(self._period)
else:
current_ts = 0
item = self._move_item(k, current_ts, _marker)
STRICT and _assert(self._data.has_key(current_ts))
if item is _marker:
raise KeyError, k
return self._wrap(item)
def __setitem__(self, k, v):
DEBUG and TLOG('__setitem__: called with key %s, value %s' % (k,v))
if self._timeout_slices:
current_ts = getCurrentTimeslice(self._period)
else:
current_ts = 0
item = self._move_item(k, current_ts, _marker)
STRICT and _assert(self._data.has_key(current_ts))
if item is _marker:
# the key didnt already exist, this is a new item
length = self._length() # XXX ReadConflictError hotspot
if self._limit and length >= self._limit:
LOG.warn('Transient object container %s max subobjects '
'reached' % self.getId())
raise MaxTransientObjectsExceeded, (
"%s exceeds maximum number of subobjects %s" %
(length, self._limit))
self._length.increment(1)
DEBUG and TLOG('__setitem__: placing value for key %s in bucket %s' %
(k, current_ts))
current_bucket = self._data[current_ts]
current_bucket[k] = v
if not issubclass(BUCKET_CLASS, Persistent):
# tickle persistence machinery
self._data[current_ts] = current_bucket
self.notifyAdd(v)
# change the TO's last accessed time
# dont use hasattr here (it hides conflict errors)
if getattr(v, 'setLastAccessed', None):
v.setLastAccessed()
def __delitem__(self, k):
DEBUG and TLOG('__delitem__ called with key %s' % k)
if self._timeout_slices:
current_ts = getCurrentTimeslice(self._period)
else:
current_ts = 0
item = self._move_item(k, current_ts)
STRICT and _assert(self._data.has_key(current_ts))
bucket = self._data[current_ts]
del bucket[k]
if not issubclass(BUCKET_CLASS, Persistent):
# tickle persistence machinery
self._data[current_ts] = bucket
# XXX does increment(-1) make any sense here?
# rationale from dunny: we are removing an item rather than simply
# declaring it to be unused?
self._length.increment(-1)
return current_ts, item
def __len__(self):
return self._length()
security.declareProtected(ACCESS_TRANSIENTS_PERM, 'get')
def get(self, k, default=None):
DEBUG and TLOG('get: called with key %s, default %s' % (k, default))
if self._timeout_slices:
current_ts = getCurrentTimeslice(self._period)
else:
current_ts = 0
item = self._move_item(k, current_ts, default)
STRICT and _assert(self._data.has_key(current_ts))
if item is default:
DEBUG and TLOG('get: returning default')
return default
return self._wrap(item)
security.declareProtected(ACCESS_TRANSIENTS_PERM, 'has_key')
def has_key(self, k):
if self._timeout_slices:
current_ts = getCurrentTimeslice(self._period)
else:
current_ts = 0
DEBUG and TLOG('has_key: calling _move_item with %s' % str(k))
item = self._move_item(k, current_ts, _marker)
DEBUG and TLOG('has_key: _move_item returned %s%s' %
(item, item is _marker and ' (marker)' or ''))
STRICT and _assert(self._data.has_key(current_ts))
if item is not _marker:
return True
DEBUG and TLOG('has_key: returning false from for %s' % k)
return False
def _get_max_expired_ts(self, now):
return now - (self._period * (self._timeout_slices + 1))
def _in_emergency_bucket_shortage(self, now):
max_ts = self._max_timeslice()
low = now/self._period
high = max_ts/self._period
required = high <= low
return required
def _finalize(self, now):
""" Call finalization handlers for the data in each stale bucket """
if not self._timeout_slices:
DEBUG and TLOG('_finalize: doing nothing (no timeout)')
return # don't do any finalization if there is no timeout
# The nature of sessioning is that when the timeslice rolls
# over, all active threads will try to do a lot of work during
# finalization if inband housekeeping is enabled, all but one
# unnecessarily. We really don't want more than one thread at
# a time to try to finalize buckets at the same time so we try
# to lock. We give up if we can't lock immediately because it
# doesn't matter if we skip a couple of opportunities for
# finalization, as long as it gets done by some thread
# eventually. A similar pattern exists for _gc and
# _replentish.
if not self.finalize_lock.acquire(0):
DEBUG and TLOG('_finalize: could not acquire lock, returning')
return
try:
DEBUG and TLOG('_finalize: lock acquired successfully')
last_finalized = self._last_finalized_timeslice()
# we want to start finalizing from one timeslice after the
# timeslice which we last finalized.
start_finalize = last_finalized + self._period
# we want to finalize only up to the maximum expired timeslice
max_ts = self._get_max_expired_ts(now)
if start_finalize >= max_ts:
DEBUG and TLOG(
'_finalize: start_finalize (%s) >= max_ts (%s), '
'doing nothing' % (start_finalize, max_ts))
return
else:
DEBUG and TLOG(
'_finalize: start_finalize (%s) <= max_ts (%s), '
'finalization possible' % (start_finalize, max_ts))
# we don't try to avoid conflicts here by doing a "random"
# dance (ala _replentish and _gc) because it's important that
# buckets are finalized as soon as possible after they've
# expired in order to call the delete notifier "on time".
self._do_finalize_work(now, max_ts, start_finalize)
finally:
self.finalize_lock.release()
def _do_finalize_work(self, now, max_ts, start_finalize):
# this is only separated from _finalize for readability; it
# should generally not be called by anything but _finalize
DEBUG and TLOG('_do_finalize_work: entering')
DEBUG and TLOG('_do_finalize_work: now is %s' % now)
DEBUG and TLOG('_do_finalize_work: max_ts is %s' % max_ts)
DEBUG and TLOG('_do_finalize_work: start_finalize is %s' %
start_finalize)
to_finalize = list(self._data.keys(start_finalize, max_ts))
DEBUG and TLOG('_do_finalize_work: to_finalize is %s' % `to_finalize`)
delta = 0
for key in to_finalize:
_assert(start_finalize <= key)
_assert(key <= max_ts)
STRICT and _assert(self._data.has_key(key))
values = list(self._data[key].values())
DEBUG and TLOG('_do_finalize_work: values to notify from ts %s '
'are %s' % (key, `list(values)`))
delta += len(values)
for v in values:
self.notifyDel(v)
if delta:
self._length.decrement(delta)
DEBUG and TLOG('_do_finalize_work: setting _last_finalized_timeslice '
'to max_ts of %s' % max_ts)
self._last_finalized_timeslice.set(max_ts)
def _invoke_finalize_and_gc(self):
# for unit testing purposes only!
last_finalized = self._last_finalized_timeslice()
now = getCurrentTimeslice(self._period) # for unit tests
start_finalize = last_finalized + self._period
max_ts = self._get_max_expired_ts(now)
self._do_finalize_work(now, max_ts, start_finalize)
self._do_gc_work(now)
def _replentish(self, now):
""" Add 'fresh' future or current buckets """
if not self._timeout_slices:
DEBUG and TLOG('_replentish: no timeout, doing nothing')
return
# the difference between high and low naturally diminishes to
# zero as now approaches self._max_timeslice() during normal
# operations. If high <= low, it means we have no current bucket,
# so we *really* need to replentish (having a current bucket is
# an invariant for continued operation).
required = self._in_emergency_bucket_shortage(now)
lock_acquired = self.replentish_lock.acquire(0)
try:
if required:
# we're in an emergency bucket shortage, we need to
# replentish regardless of whether we got the lock or
# not. (if we didn't get the lock, this transaction
# will likely result in a conflict error, that's ok)
if lock_acquired:
DEBUG and TLOG('_replentish: required, lock acquired)')
else:
DEBUG and TLOG('_replentish: required, lock NOT acquired)')
max_ts = self._max_timeslice()
self._do_replentish_work(now, max_ts)
elif lock_acquired:
# If replentish is optional, minimize the chance that
# two threads will attempt to do replentish work at
# the same time (which causes conflicts) by
# introducing a random element.
DEBUG and TLOG('_replentish: attempting optional replentish '
'(lock acquired)')
max_ts = self._max_timeslice()
low = now/self._period
high = max_ts/self._period
if roll(low, high, 'optional replentish'):
self._do_replentish_work(now, max_ts)
else:
# This is an optional replentish and we can't acquire
# the lock, bail.
DEBUG and TLOG('_optional replentish attempt aborted, could '
'not acquire lock.')
return
finally:
if lock_acquired:
self.replentish_lock.release()
def _do_replentish_work(self, now, max_ts):
DEBUG and TLOG('_do_replentish_work: entering')
# this is only separated from _replentish for readability; it
# should generally not be called by anything but _replentish
# available_spares == the number of "spare" buckets that exist
# in "_data"
available_spares = (max_ts - now) / self._period
DEBUG and TLOG('_do_replentish_work: now = %s' % now)
DEBUG and TLOG('_do_replentish_work: max_ts = %s' % max_ts)
DEBUG and TLOG('_do_replentish_work: available_spares = %s'
% available_spares)
if available_spares >= SPARE_BUCKETS:
DEBUG and TLOG('_do_replentish_work: available_spares (%s) >= '
'SPARE_BUCKETS (%s), doing '
'nothing'% (available_spares,
SPARE_BUCKETS))
return
if max_ts < now:
# the newest bucket in self._data is older than now!
replentish_start = now
replentish_end = now + (self._period * SPARE_BUCKETS)
else:
replentish_start = max_ts + self._period
replentish_end = max_ts + (self._period * (SPARE_BUCKETS +1))
DEBUG and TLOG('_do_replentish_work: replentish_start = %s' %
replentish_start)
DEBUG and TLOG('_do_replentish_work: replentish_end = %s'
% replentish_end)
# n is the number of buckets to create
n = (replentish_end - replentish_start) / self._period
new_buckets = getTimeslices(replentish_start, n, self._period)
new_buckets.reverse()
STRICT and _assert(new_buckets)
DEBUG and TLOG('_do_replentish_work: adding %s new buckets' % n)
DEBUG and TLOG('_do_replentish_work: buckets to add = %s'
% new_buckets)
for k in new_buckets:
STRICT and _assert(not self._data.has_key(k))
self._data[k] = BUCKET_CLASS() # XXX ReadConflictError hotspot
self._max_timeslice.set(max(new_buckets))
def _gc(self, now=None):
""" Remove stale buckets """
if not self._timeout_slices:
return # dont do gc if there is no timeout
# give callers a good chance to do nothing (gc isn't as important
# as replentishment or finalization)
if not roll(0, 5, 'gc'):
DEBUG and TLOG('_gc: lost roll, doing nothing')
return
if not self.gc_lock.acquire(0):
DEBUG and TLOG('_gc: couldnt acquire lock')
return
try:
if now is None:
now = getCurrentTimeslice(self._period) # for unit tests
last_gc = self._last_gc_timeslice()
gc_every = self._period * round(SPARE_BUCKETS / 2.0)
if (now - last_gc) < gc_every:
DEBUG and TLOG('_gc: gc attempt not yet required '
'( (%s - %s) < %s )' % (now, last_gc, gc_every))
return
else:
DEBUG and TLOG(
'_gc: (%s -%s) > %s, gc invoked' % (now, last_gc,
gc_every))
self._do_gc_work(now)
finally:
self.gc_lock.release()
def _do_gc_work(self, now):
# this is only separated from _gc for readability; it should
# generally not be called by anything but _gc
# we garbage collect any buckets that have already been run
# through finalization
DEBUG and TLOG('_do_gc_work: entering')
max_ts = self._last_finalized_timeslice()
DEBUG and TLOG('_do_gc_work: max_ts is %s' % max_ts)
to_gc = list(self._data.keys(None, max_ts))
DEBUG and TLOG('_do_gc_work: to_gc is: %s' % str(to_gc))
for key in to_gc:
_assert(key <= max_ts)
STRICT and _assert(self._data.has_key(key))
DEBUG and TLOG('_do_gc_work: deleting %s from _data' % key)
del self._data[key]
DEBUG and TLOG('_do_gc_work: setting last_gc_timeslice to %s' % now)
self._last_gc_timeslice.set(now)
def notifyAdd(self, item):
DEBUG and TLOG('notifyAdd with %s' % item)
callback = self._getCallback(self._addCallback)
if callback is None:
return
self._notify(item, callback, 'notifyAdd')
def notifyDel(self, item):
DEBUG and TLOG('notifyDel with %s' % item)
callback = self._getCallback(self._delCallback)
if callback is None:
return
self._notify(item, callback, 'notifyDel' )
def _getCallback(self, callback):
if not callback:
return None
if type(callback) is type(''):
try:
method = self.unrestrictedTraverse(callback)
except (KeyError, AttributeError):
path = self.getPhysicalPath()
err = 'No such onAdd/onDelete method %s referenced via %s'
LOG.warn(err % (callback, '/'.join(path)),
exc_info=sys.exc_info())
return
else:
method = callback
return method
def _notify(self, item, callback, name):
if callable(callback):
sm = getSecurityManager()
try:
user = sm.getUser()
try:
newSecurityManager(None, nobody)
callback(item, self)
except:
# dont raise, just log
path = self.getPhysicalPath()
LOG.warn('%s failed when calling %s in %s' % (name,callback,
'/'.join(path)),
exc_info=sys.exc_info())
finally:
setSecurityManager(sm)
else:
err = '%s in %s attempted to call non-callable %s'
path = self.getPhysicalPath()
LOG.warn(err % (name, '/'.join(path), callback),
exc_info=sys.exc_info())
def getId(self):
return self.id
security.declareProtected(CREATE_TRANSIENTS_PERM, 'new_or_existing')
def new_or_existing(self, key):
DEBUG and TLOG('new_or_existing called with %s' % key)
item = self.get(key, _marker)
if item is _marker:
item = TransientObject(key)
self[key] = item
item = self._wrap(item)
return item
security.declareProtected(CREATE_TRANSIENTS_PERM, 'new')
def new(self, key):
DEBUG and TLOG('new called with %s' % key)
if type(key) is not type(''):
raise TypeError, (key, "key is not a string type")
if self.has_key(key):
raise KeyError, "cannot duplicate key %s" % key
item = TransientObject(key)
self[key] = item
return self._wrap(item)
# TransientItemContainer methods
security.declareProtected(MANAGE_CONTAINER_PERM, 'setTimeoutMinutes')
def setTimeoutMinutes(self, timeout_mins, period_secs=20):
""" The period_secs parameter is defaulted to preserve backwards API
compatibility. In older versions of this code, period was
hardcoded to 20. """
timeout_secs = timeout_mins * 60
if (timeout_mins != self.getTimeoutMinutes()
or period_secs != self.getPeriodSeconds()):
# do nothing unless something has changed
self._setTimeout(timeout_mins, period_secs)
self._reset()
def getTimeoutMinutes(self):
""" """
return self._timeout_secs / 60
def getPeriodSeconds(self):
""" """
return self._period
security.declareProtected(MGMT_SCREEN_PERM, 'getSubobjectLimit')
def getSubobjectLimit(self):
""" """
return self._limit
security.declareProtected(MANAGE_CONTAINER_PERM, 'setSubobjectLimit')
def setSubobjectLimit(self, limit):
""" """
if limit != self.getSubobjectLimit():
self._setLimit(limit)
security.declareProtected(MGMT_SCREEN_PERM, 'getAddNotificationTarget')
def getAddNotificationTarget(self):
return self._addCallback or ''
security.declareProtected(MANAGE_CONTAINER_PERM,'setAddNotificationTarget')
def setAddNotificationTarget(self, f):
self._addCallback = f
security.declareProtected(MGMT_SCREEN_PERM, 'getDelNotificationTarget')
def getDelNotificationTarget(self):
return self._delCallback or ''
security.declareProtected(MANAGE_CONTAINER_PERM,'setDelNotificationTarget')
def setDelNotificationTarget(self, f):
self._delCallback = f
security.declareProtected(MGMT_SCREEN_PERM, 'disableInbandHousekeeping')
def disableInbandHousekeeping(self):
""" No longer perform inband housekeeping """
self._inband_housekeeping = False
security.declareProtected(MGMT_SCREEN_PERM, 'enableInbandHousekeeping')
def enableInbandHousekeeping(self):
""" (Re)enable inband housekeeping """
self._inband_housekeeping = True
security.declareProtected(MGMT_SCREEN_PERM, 'isInbandHousekeepingEnabled')
def isInbandHousekeepingEnabled(self):
""" Report if inband housekeeping is enabled """
return self._inband_housekeeping
security.declareProtected('View', 'housekeep')
def housekeep(self):
""" Call this from a scheduler at least every
self._period * (SPARE_BUCKETS - 1) seconds to perform out of band
housekeeping """
# we can protect this method from being called too often by
# anonymous users as necessary in the future; we already have a lot
# of protection as-is though so no need to make it more complicated
# than necessary at the moment
self._housekeep(getCurrentTimeslice(self._period))
def _housekeep(self, now):
self._finalize(now)
self._replentish(now)
self._gc(now)
security.declareProtected(MANAGE_CONTAINER_PERM,
'manage_changeTransientObjectContainer')
def manage_changeTransientObjectContainer(
self, title='', timeout_mins=20, addNotification=None,
delNotification=None, limit=0, period_secs=20, REQUEST=None
):
""" Change an existing transient object container. """
self.title = title
self.setTimeoutMinutes(timeout_mins, period_secs)
self.setSubobjectLimit(limit)
if not addNotification:
addNotification = None
if not delNotification:
delNotification = None
self.setAddNotificationTarget(addNotification)
self.setDelNotificationTarget(delNotification)
if REQUEST is not None:
return self.manage_container(
self, REQUEST, manage_tabs_message='Changes saved.'
)
def __setstate__(self, state):
# upgrade versions of Transience in Zope versions less
# than 2.7.1, which used a different transience mechanism. Note:
# this will not work for upgrading versions older than 2.6.0,
# all of which used a very different transience implementation
# can't make __len__ an instance variable in new-style classes
# f/w compat: 2.8 cannot use __len__ as an instance variable
if not state.has_key('_length'):
length = state.get('__len__', Length2())
self._length = self.getLen = length
oldlength = state['_length']
if isinstance(oldlength, BTreesLength):
# TOCS prior to 2.7.3 had a BTrees.Length.Length object as
# the TOC length object, replace it with our own Length2
# that does our conflict resolution correctly:
sz = oldlength()
self._length = self.getLen = Length2(sz)
# TOCs prior to 2.7.1 took their period from a global
if not state.has_key('_period'):
self._period = 20 # this was the default for all prior releases
# TOCs prior to 2.7.1 used a different set of data structures
# for efficiently keeping tabs on the maximum slice
if not state.has_key('_max_timeslice'):
new_slices = getTimeslices(
getCurrentTimeslice(self._period),
SPARE_BUCKETS*2,
self._period)
for i in new_slices:
if not self._data.has_key(i):
self._data[i] = BUCKET_CLASS()
# create an Increaser for max timeslice
self._max_timeslice = Increaser(max(new_slices))
if not state.has_key('_last_finalized_timeslice'):
self._last_finalized_timeslice = Increaser(-self._period)
# TOCs prior to 2.7.3 didn't have a _last_gc_timeslice
if not state.has_key('_last_gc_timeslice'):
self._last_gc_timeslice = Increaser(-self._period)
# we should probably delete older attributes from state such as
# '_last_timeslice', '_deindex_next',and '__len__' here but we leave
# them in order to allow people to switch between 2.6.0->2.7.0 and
# 2.7.1+ as necessary (although that has not been tested)
self.__dict__.update(state)
def getCurrentTimeslice(period):
"""
Return an integer representing the 'current' timeslice.
The current timeslice is guaranteed to be the same integer
within a 'slice' of time based on a divisor of 'self._period'.
'self._period' is the number of seconds in a slice.
"""
now = time.time()
low = int(math.floor(now)) - period + 1
high = int(math.ceil(now)) + 1
for x in range(low, high):
if x % period == 0:
return x
def getTimeslices(begin, n, period):
""" Get a list of future timeslice integers of 'n' size in descending
order """
l = []
for x in range(n):
l.insert(0, begin + (x * period))
return l
def roll(low, high, reason):
try:
result = random.randrange(low, high)
except ValueError:
# empty range, must win this roll
result = low
if result == low:
DEBUG and TLOG('roll: low: %s, high: %s: won with %s (%s)' %
(low, high, result, reason))
return True
else:
DEBUG and TLOG('roll: low: %s, high: %s: lost with %s (%s)' %
(low, high, result, reason))
return False
def _assert(case):
if not case:
raise AssertionError
class Increaser(Persistent):
"""
A persistent object representing a typically increasing integer that
has conflict resolution which uses the greatest integer out of the three
available states.
"""
def __init__(self, v):
self.value = v
def set(self, v):
self.value = v
def __getstate__(self):
return self.value
def __setstate__(self, v):
self.value = v
def __call__(self):
return self.value
def _p_resolveConflict(self, old, state1, state2):
return max(old, state1, state2)
class Length2(Persistent):
"""
A persistent object responsible for maintaining a repesention of
the number of current transient objects.
Conflict resolution is sensitive to which methods are used to
change the length.
"""
def __init__(self, value=0):
self.set(value)
def set(self, value):
self.value = value
self.floor = 0
self.ceiling = value
def increment(self, delta):
"""Increase the length by delta.
Conflict resolution will take the sum of all the increments."""
self.ceiling += delta
self.value += delta
def decrement(self, delta):
"""Decrease the length by delta.
Conflict resolution will take the highest decrement."""
self.floor += delta
self.value -= delta
def __getstate__(self):
return self.__dict__
def __setstate__(self, state):
self.__dict__.update(state)
def __call__(self):
return self.value
def _p_resolveConflict(self, old, saved, new):
new['ceiling'] = saved['ceiling'] + new['ceiling'] - old['ceiling']
new['floor'] = max(old['floor'], saved['floor'], new['floor'])
new['value'] = new['ceiling'] - new['floor']
return new
InitializeClass(TransientObjectContainer)
##########################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##########################################################################
"""
Transient Objects
TransientObjectContainers are objects which contain zero or more
TransientObjects. They implement the following interfaces:
- ItemWithId
- StringKeyedHomogenousItemContainer
- TransientItemContainer
In particular, one uses the 'new_or_existing' method on
TransientObjectContainers to retrieve or create a TransientObject
based on a given string key.
If add or delete notifications are registered with the container,
they will be called back when items in the container are added or
deleted, with the item and the container as arguments. The
callbacks may be registered either as bound methods, functions, or
physical paths to Zope Script (Python Script or External Method)
objects (e.g. '/some/resolvable/script/name'). In any of these
cases, the delete and add notifications will be called with
arguments allowing the callbacks to operate on data representing the
state of the transient object at the moment of addition or deletion
(see setAddNotificationTarget and setDelNotificationTarget below).
TransientObjects are containerish items held within
TransientObjectContainers and they implement the following
interfaces:
- ItemWithId
- Transient
- DictionaryLike
- TTWDictionary
- ImmutablyValuedMappingOfPickleableObjects
Of particular importance is the idea that TransientObjects do not
offer the contract of "normal" ZODB container objects; mutations
made to items which are contained within a TransientObject cannot be
expected to persist. Developers need explicitly resave the state of
a subobject of a TransientObject by placing it back into the
TransientObject via the TransientObject.__setitem__ or .set methods.
This requirement is due to the desire to allow people to create
alternate TransientObject implementations that are *not* based on
the ZODB. Practically, this means that when working with a
TransientObject which contains mutable subobjects (even if they
inherit from Persistence.Persistent), you *must* resave them back
into the TransientObject. For example::
class Foo(Persistence.Persistent):
pass
transient_object = transient_data_container.new('t')
foo = transient_object['foo'] = Foo()
foo.bar = 1
# the following is *necessary* to repersist the data
transient_object['foo'] = foo
"""
from zope.interface import Interface
class Transient(Interface):
def invalidate():
"""
Invalidate (expire) the transient object.
Causes the transient object container's "before destruct" method
related to this object to be called as a side effect.
"""
def isValid():
"""
Return true if transient object is still valid, false if not.
A transient object is valid if its invalidate method has not been
called.
"""
def getLastAccessed():
"""
Return the time the transient object was last accessed in
integer seconds-since-the-epoch form. Last accessed time
is defined as the last time the transient object's container
"asked about" this transient object.
"""
def setLastAccessed():
"""
Cause the last accessed time to be set to now.
"""
def getLastModified():
"""
Return the time the transient object was last modified in
integer seconds-since-the-epoch form. Modification generally implies
a call to one of the transient object's __setitem__ or __delitem__
methods, directly or indirectly as a result of a call to
update, clear, or other mutating data access methods.
"""
def setLastModified():
"""
Cause the last modified time to be set to now.
"""
def getCreated():
"""
Return the time the transient object was created in integer
seconds-since-the-epoch form.
"""
def getContainerKey():
"""
Return the key under which the object was placed in its
container.
"""
class DictionaryLike(Interface):
def keys():
"""
Return sequence of key elements.
"""
def values():
"""
Return sequence of value elements.
"""
def items():
"""
Return sequence of (key, value) elements.
"""
def get(k, default='marker'):
"""
Return value associated with key k. Return None or default if k
does not exist.
"""
def has_key(k):
"""
Return true if item referenced by key k exists.
"""
def clear():
"""
Remove all key/value pairs.
"""
def update(d):
"""
Merge dictionary d into ourselves.
"""
# DictionaryLike does NOT support copy()
class ItemWithId(Interface):
def getId():
"""
Returns a meaningful unique id for the object. Note that this id
need not the key under which the object is stored in its container.
"""
class TTWDictionary(DictionaryLike, ItemWithId):
def set(k, v):
"""
Call __setitem__ with key k, value v.
"""
def delete(k):
"""
Call __delitem__ with key k.
"""
def __guarded_setitem__(k, v):
"""
Call __setitem__ with key k, value v.
"""
class ImmutablyValuedMappingOfPickleableObjects(Interface):
def __setitem__(k, v):
"""
Sets key k to value v, if k is both hashable and pickleable and
v is pickleable, else raise TypeError.
"""
def __getitem__(k):
"""
Returns the value associated with key k.
Note that no guarantee is made to persist changes made to mutable
objects obtained via __getitem__, even if they support the ZODB
Persistence interface. In order to ensure that changes to mutable
values are persisted, you need to explicitly put the value back in
to the mapping via __setitem__.
"""
def __delitem__(k):
"""
Remove the key/value pair related to key k.
"""
class HomogeneousItemContainer(Interface):
"""
An object which:
1. Contains zero or more subobjects, all of the same type.
2. Is responsible for the creation of its subobjects.
3. Allows for the access of a subobject by key.
"""
def get(k, default=None):
"""
Return value associated with key k via __getitem__. If value
associated with k does not exist, return default.
Returned item is acquisition-wrapped in self unless a default
is passed in and returned.
"""
def has_key(k):
"""
Return true if container has value associated with key k, else
return false.
"""
class StringKeyedHomogeneousItemContainer(HomogeneousItemContainer):
def new(k):
"""
Creates a new subobject of the type supported by this container
with key "k" and returns it.
If an object already exists in the container with key "k", a
KeyError is raised.
"k" must be a string, else a TypeError is raised.
If the container is 'full', a MaxTransientObjectsExceeded exception
will be raised.
Returned object is acquisition-wrapped in self.
"""
def new_or_existing(k):
"""
If an object already exists in the container with key "k", it
is returned.
Otherwise, create a new subobject of the type supported by this
container with key "k" and return it.
"k" must be a string, else a TypeError is raised.
If a new object needs to be created and the container is 'full',
a MaxTransientObjectsExceeded exception will be raised.
Returned object is acquisition-wrapped in self.
"""
class TransientItemContainer(Interface):
def setTimeoutMinutes(timeout_mins):
"""
Set the number of minutes of inactivity allowable for subobjects
before they expire.
"""
def getTimeoutMinutes():
"""
Return the number of minutes allowed for subobject inactivity
before expiration.
"""
def getAddNotificationTarget():
"""
Returns the currently registered 'add notification' value, or None.
"""
def setAddNotificationTarget(f):
"""
Cause the 'add notification' function to be 'f'.
If 'f' is not callable and is a string, treat it as a physical
path to a Zope Script object (Python Script, External Method,
et. al).
'add notify' functions need accept two arguments: 'item',
which is the transient object being destroyed, and 'container',
which is the transient object container which is performing
the destruction. For example::
def addNotify(item, container):
print "id of 'item' arg was %s" % item.getId()
"""
def getDelNotificationTarget():
"""
Returns the currently registered 'delete notification' value, or
None.
"""
def setDelNotificationTarget(f):
"""
Cause the 'delete notification' function to be 'f'.
If 'f' is not callable and is a string, treat it as a physical
path to a Zope Script object (Python Script, External Method,
et. al).
'Before destruction' functions need accept two arguments: 'item',
which is the transient object being destroyed, and 'container',
which is the transient object container which is performing
the destruction. For example::
def delNotify(item, container):
print "id of 'item' arg was %s" % item.getId()
"""
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
"""Simple ZODB-based transient object implementation.
"""
import logging
import os
import random
import sys
import thread
import time
from AccessControl.class_init import InitializeClass
from AccessControl.SecurityInfo import ClassSecurityInfo
from Acquisition import Implicit
from Persistence import Persistent
from ZODB.POSException import ConflictError
from zope.interface import implements
from Products.Transience.TransienceInterfaces import DictionaryLike
from Products.Transience.TransienceInterfaces import \
ImmutablyValuedMappingOfPickleableObjects
from Products.Transience.TransienceInterfaces import ItemWithId
from Products.Transience.TransienceInterfaces import Transient
from Products.Transience.TransienceInterfaces import TransientItemContainer
from Products.Transience.TransienceInterfaces import TTWDictionary
DEBUG = int(os.environ.get('Z_TOC_DEBUG', 0))
LOG = logging.getLogger('Zope.TransientObject')
def TLOG(*args):
sargs = []
sargs.append(str(thread.get_ident()))
sargs.append(str(time.time()))
for arg in args:
sargs.append(str(arg))
msg = ' '.join(sargs)
LOG.info(msg)
_notfound = []
WRITEGRANULARITY=30 # Timing granularity for access write clustering, seconds
class TransientObject(Persistent, Implicit):
""" Dictionary-like object that supports additional methods
concerning expiration and containment in a transient object container
"""
implements(ItemWithId, # randomly generate an id
Transient,
DictionaryLike,
TTWDictionary,
ImmutablyValuedMappingOfPickleableObjects
)
security = ClassSecurityInfo()
security.setDefaultAccess('allow')
security.declareObjectPublic()
_last_modified = None
# _last modified indicates the last time that __setitem__, __delitem__,
# update or clear was called on us.
def __init__(self, containerkey):
self.token = containerkey
self.id = self._generateUniqueId()
self._container = {}
self._created = self._last_accessed = time.time()
# _last_accessed indicates the last time that *our container
# was asked about us* (NOT the last time __getitem__ or get
# or any of our other invariant data access methods are called).
# Our container manages our last accessed time, we don't much
# concern ourselves with it other than exposing an interface
# to set it on ourselves.
# -----------------------------------------------------------------
# ItemWithId
#
def getId(self):
return self.id
# -----------------------------------------------------------------
# Transient
#
def invalidate(self):
# hasattr hides conflicts
if getattr(self, '_invalid', _notfound) is not _notfound:
# we dont want to invalidate twice
return
trans_ob_container = None
# search our acquisition chain for a transient object container
# and delete ourselves from it.
for ob in getattr(self, 'aq_chain', []):
if TransientItemContainer.providedBy(ob):
trans_ob_container = ob
break
if trans_ob_container is not None:
if trans_ob_container.has_key(self.token):
del trans_ob_container[self.token]
self._invalid = None
def isValid(self):
# hasattr hides conflicts
if getattr(self, '_invalid', _notfound) is _notfound:
return 1
def getLastAccessed(self):
return self._last_accessed
def setLastAccessed(self):
# check to see if the last_accessed time is too recent, and avoid
# setting if so, to cut down on heavy writes
t = time.time()
if (self._last_accessed + WRITEGRANULARITY) < t:
self._last_accessed = t
def getLastModified(self):
return self._last_modified
def setLastModified(self):
self._last_modified = time.time()
def getCreated(self):
return self._created
def getContainerKey(self):
return self.token
# -----------------------------------------------------------------
# DictionaryLike
#
def keys(self):
return self._container.keys()
def values(self):
return self._container.values()
def items(self):
return self._container.items()
def get(self, k, default=_notfound):
v = self._container.get(k, default)
if v is _notfound: return None
return v
def has_key(self, k):
if self._container.get(k, _notfound) is not _notfound: return 1
return 0
def clear(self):
self._p_changed = 1
self._container.clear()
self.setLastModified()
def update(self, d):
self._p_changed = 1
for k in d.keys():
self[k] = d[k]
# -----------------------------------------------------------------
# ImmutablyValuedMappingOfPickleableObjects (what a mouthful!)
#
def __setitem__(self, k, v):
self._p_changed = 1
self._container[k] = v
self.setLastModified()
def __getitem__(self, k):
return self._container[k]
def __delitem__(self, k):
self._p_changed = 1
del self._container[k]
self.setLastModified()
# -----------------------------------------------------------------
# TTWDictionary
#
set = __setitem__
__guarded_setitem__ = __setitem__
__guarded_delitem__ = __delitem__
delete = __delitem__
# -----------------------------------------------------------------
# Other non interface code
#
def _p_resolveConflict(self, saved, state1, state2):
DEBUG and TLOG('entering TO _p_rc')
DEBUG and TLOG('states: sv: %s, s1: %s, s2: %s' % (
saved, state1, state2))
states = [saved, state1, state2]
# We can clearly resolve the conflict if one state is invalid,
# because it's a terminal state.
for state in states:
if state.has_key('_invalid'):
DEBUG and TLOG('TO _p_rc: a state was invalid')
return state
# The only other times we can clearly resolve the conflict is if
# the token, the id, or the creation time don't differ between
# the three states, so we check that here. If any differ, we punt
# by raising ConflictError.
attrs = ['token', 'id', '_created']
for attr in attrs:
svattr = saved.get(attr)
s1attr = state1.get(attr)
s2attr = state2.get(attr)
DEBUG and TLOG('TO _p_rc: attr %s: sv: %s s1: %s s2: %s' %
(attr, svattr, s1attr, s2attr))
if not svattr==s1attr==s2attr:
DEBUG and TLOG('TO _p_rc: cant resolve conflict')
raise ConflictError
# Now we need to do real work.
#
# Data in our _container dictionaries might conflict. To make
# things simple, we intentionally create a race condition where the
# state which was last modified "wins". It would be preferable to
# somehow merge our _containers together, but as there's no
# generally acceptable way to union their states, there's not much
# we can do about it if we want to be able to resolve this kind of
# conflict.
# We return the state which was most recently modified, if
# possible.
states.sort(lastmodified_sort)
if states[0].get('_last_modified'):
DEBUG and TLOG('TO _p_rc: returning last mod state')
return states[0]
# If we can't determine which object to return on the basis
# of last modification time (no state has been modified), we return
# the object that was most recently accessed (last pulled out of
# our parent). This will return an essentially arbitrary state if
# all last_accessed values are equal.
states.sort(lastaccessed_sort)
DEBUG and TLOG('TO _p_rc: returning last_accessed state')
return states[0]
getName = getId # this is for SQLSession compatibility
def _generateUniqueId(self):
t = str(int(time.time()))
d = "%010d" % random.randint(0, sys.maxint-1)
return "%s%s" % (t, d)
def __repr__(self):
return "id: %s, token: %s, content keys: %s" % (
self.id, self.token, `self.keys()`
)
def lastmodified_sort(d1, d2):
""" sort dictionaries in descending order based on last mod time """
m1 = d1.get('_last_modified', 0)
m2 = d2.get('_last_modified', 0)
if m1 == m2: return 0
if m1 > m2: return -1 # d1 is "less than" d2
return 1
def lastaccessed_sort(d1, d2):
""" sort dictionaries in descending order based on last access time """
m1 = d1.get('_last_accessed', 0)
m2 = d2.get('_last_accessed', 0)
if m1 == m2: return 0
if m1 > m2: return -1 # d1 is "less than" d2
return 1
InitializeClass(TransientObject)
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
"""
Transience initialization routines
"""
import ZODB # this is to help out testrunner, don't remove.
import Transience
# import of MaxTransientObjectsExceeded for easy import from scripts,
# this is protected by a module security info declaration in the
# Sessions package.
from Transience import MaxTransientObjectsExceeded
def initialize(context):
context.registerClass(
Transience.TransientObjectContainer,
permission=Transience.ADD_CONTAINER_PERM,
constructors=(Transience.constructTransientObjectContainerForm,
Transience.constructTransientObjectContainer)
)
<dtml-var manage_page_header>
<dtml-var "manage_form_title(this(), _,
form_title='Add Transient Object Container'
)">
<FORM ACTION="constructTransientObjectContainer" METHOD="POST">
<TABLE CELLSPACING="2">
<tr>
<div class="form-help">
<td colspan="2">
<p>
Transient Object Containers are used to store transient data.
Transient data will persist, but only for a user-specified period of time,
(the "data object timeout") after which it will be flushed.
</p>
<p>
It is recommended that Transient Object Containers be added to storages which
do not support undo operations; transient objects are write-intensive;
their use may cause many undoable transactions, potentially bloating
undoing ZODB databases.
</p>
<p>
Transient Object Containers support <b>Add and Delete Scripts</b> which
are methods which are invoked when transient objects are added or deleted
from the container. A add/delete script is invoked with the item being
operated upon and the transient object container as arguments. Specify
the Zope physical path to the method to be invoked to receive the notification
(e.g. '/folder/add_notifier').
</p>
</div>
</td>
</tr>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Id
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="id" SIZE="20">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Title (optional)
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="title" SIZE="40">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Data object timeout (in minutes)
</div>
<div class="form-help">
("0" means no expiration)
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="timeout_mins:int" SIZE="10" value="20">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Timeout resolution (in seconds)
</div>
<div class="form-help">
(accept the default if you're not sure)
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="period_secs:int" SIZE="10" value="20">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Maximum number of subobjects
</div>
<div class="form-help">
("0" means infinite)
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="limit:int" SIZE="10" value="1000">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Script to call upon object add (optional)
</div>
<div class="form-help">
(e.g. "/somefolder/addScript")
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="addNotification" SIZE="40">
</TD>
</TR>
<TR>
<TD ALIGN="LEFT" VALIGN="TOP">
<div class="form-label">
Script to call upon object delete (optional)
</div>
<div class="form-help">
(e.g. "/somefolder/delScript")
</div>
</TD>
<TD ALIGN="LEFT" VALIGN="TOP">
<INPUT TYPE="TEXT" NAME="delNotification" SIZE="40">
</TD>
</TR>
<TR>
<TD>
</TD>
<TD> <BR><INPUT class="form-element" TYPE="SUBMIT" VALUE=" Add "> </TD>
</TR>
</TABLE>
</FORM>
<dtml-var manage_page_footer>
<dtml-var manage_page_header>
<dtml-var "manage_tabs(this(), _,
form_title='Change Transient Object Container'
)">
<form action="manage_changeTransientObjectContainer" method="post">
<p class="form-help">
Transient Object Containers are used to store transient data.
Transient data will persist, but only for a user-specified period of time
(the "data object timeout") after which it will be flushed.
</p>
<dtml-call housekeep><!-- turn the buckets if necessary -->
<p class="form-label">
<font color="green">
<dtml-let l=getLen>
<dtml-if l>
<dtml-if "l == 1">1 item is in this transient object container.
<dtml-else>&dtml-l; items are in this transient object container.
</dtml-if>
<dtml-else>
There are no items in this transient object container.
</dtml-if>
</dtml-let>
</font>
</p>
<table cellspacing="2">
<tr>
<td align="left" valign="top">
<div class="form-label">
Title
</div>
</td>
<td align="left" valign="top">
<input type="text" name="title" size=30 value="&dtml-title;">
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Data object timeout value (in minutes)
</div>
<div class="form-help">
("0" means no expiration)
</div>
</td>
<td align="left" valign="top">
<input type="text" name="timeout_mins:int" size=10
value=&dtml-getTimeoutMinutes;>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Timeout resolution (in seconds)
</div>
<div class="form-help">
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. This number must divide evenly
into the number of timeout seconds ("Data object timeout value" * 60)
and cannot be set higher than the timeout value in seconds.
</div>
</td>
<td align="left" valign="top">
<input type="text" name="period_secs:int" size=10
value=&dtml-getPeriodSeconds;>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Maximum number of subobjects
</div>
<div class="form-help">
("0" means infinite)
</div>
</td>
<td align="left" valign="top">
<input type="text" name="limit:int" size=10
value=&dtml-getSubobjectLimit;>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Script to call when objects are added
</div>
<div class="form-help">
(e.g. "/somefolder/addScript")
</div>
</td>
<td align="left" valign="top">
<input type="text" name="addNotification"
value="&dtml-getAddNotificationTarget;" size=40>
</td>
</tr>
<tr>
<td align="left" valign="top">
<div class="form-label">
Script to call when objects are deleted
</div>
<div class="form-help">
(e.g. "/somefolder/delScript")
</div>
</td>
<td align="left" valign="top">
<input type="text" name="delNotification"
value="&dtml-getDelNotificationTarget;" size=40>
</td>
</tr>
<dtml-let l=getLen>
<dtml-if l>
<tr>
<td colspan=2>
<br/>
<p class="form-label">
<font color="red">WARNING!</font>
All data objects existing in this transient object container
will be deleted when the data object timeout or expiration resolution
is changed.
</p>
</tr>
</td>
</dtml-if>
</dtml-let>
<tr>
<td></td>
<td>
<input class="form-element" type="submit" name="submit" value=" Change ">
</td>
</tr>
</table>
</form>
<dtml-var manage_page_footer>
##############################################################################
#
# Copyright (c) 2003 Zope Foundation 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.
#
##############################################################################
# This file is needed to make this a package.
import sys
import time as origtime
epoch = origtime.time()
resolution = 120.0
timeout = 30
if sys.platform[:3].lower() == "win":
resolution = 60.0
timeout = 60
def time():
""" False timer -- returns time R x faster than normal time """
return (origtime.time() - epoch) * resolution
def sleep(duration):
""" False sleep -- sleep for 1/R the time specifed """
origtime.sleep(duration / resolution)
import time as origtime
epoch = origtime.time()
def time():
""" False timer -- returns time 60 x faster than normal time """
return (origtime.time() - epoch) * 60
def sleep(duration):
""" False sleep -- sleep for 1/60 the time specifed """
origtime.sleep(duration / 60)
import ZODB # in order to get Persistence.Persistent working
import transaction
from Testing import makerequest
from Products.Transience.Transience import TransientObjectContainer
import Products.Transience.Transience
import Products.Transience.TransientObject
from unittest import TestCase, TestSuite, makeSuite
from ZODB.DemoStorage import DemoStorage
from OFS.Application import Application
import fauxtime
import time as oldtime
WRITEGRANULARITY = 30
stuff = {}
def _getApp():
app = stuff.get('app', None)
if not app:
ds = DemoStorage()
db = ZODB.DB(ds)
conn = db.open()
root = conn.root()
app = Application()
root['Application']= app
transaction.commit()
stuff['app'] = app
stuff['conn'] = conn
stuff['db'] = db
return app
def _openApp():
conn = stuff['db'].open()
root = conn.root()
app = root['Application']
return conn, app
def _delApp():
transaction.abort()
stuff['conn'].close()
del stuff['conn']
del stuff['app']
del stuff['db']
class TestBase(TestCase):
def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
Products.Transience.Transience.setStrict(1)
self.app = makerequest.makerequest(_getApp())
timeout = self.timeout = 1
sm=TransientObjectContainer(
id='sm', timeout_mins=timeout, title='SessionThing',
addNotification=addNotificationTarget,
delNotification=delNotificationTarget)
self.app._setObject('sm', sm)
def tearDown(self):
transaction.abort()
_delApp()
del self.app
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
Products.Transience.Transience.setStrict(0)
class TestLastAccessed(TestBase):
def testLastAccessed(self):
sdo = self.app.sm.new_or_existing('TempObject')
la1 = sdo.getLastAccessed()
# time.time() on Windows has coarse granularity (updates at
# 18.2 Hz -- about once each 0.055 seconds). We have to sleep
# long enough so that "the next" call to time.time() actually
# delivers a larger value. _last_accessed isn't actually updated
# unless current time.time() is greater than the last value +
# WRITEGRANULARITY. The time() and sleep() are fudged by a
# factor of 60, though. The code here used to do
# fauxtime.sleep(WRITEGRANULARITY + 1)
# and that wasn't enough on Windows. The "+1" only added 1/60th
# of a second sleep time in real time, much less than the Windows
# time.time() resolution. Rounding up 0.055 to 1 digit and
# multiplying by 60 ensures that we'll actually sleep long enough
# to get to the next Windows time.time() tick.
fauxtime.sleep(WRITEGRANULARITY + 0.06 * 60)
sdo = self.app.sm.get('TempObject')
self.assert_(sdo.getLastAccessed() > la1)
class TestNotifications(TestBase):
def testAddNotification(self):
self.app.sm.setAddNotificationTarget(addNotificationTarget)
sdo = self.app.sm.new_or_existing('TempObject')
now = fauxtime.time()
k = sdo.get('starttime')
self.assertEqual(type(k), type(now))
self.assert_(k <= now)
def testDelNotification(self):
self.app.sm.setDelNotificationTarget(delNotificationTarget)
sdo = self.app.sm.new_or_existing('TempObject')
# sleep longer than timeout
fauxtime.sleep(self.timeout * 100.0)
self.app.sm.get('TempObject')
now = fauxtime.time()
k = sdo.get('endtime')
self.assertEqual(type(k), type(now))
self.assert_(k <= now)
def testMissingCallbackGetCallbackReturnsNone(self):
# in response to http://zope.org/Collectors/Zope/1403
self.assertEqual(None, self.app.sm._getCallback('/foo/bar/baz'))
def addNotificationTarget(item, context):
item['starttime'] = fauxtime.time()
def delNotificationTarget(item, context):
item['endtime'] = fauxtime.time()
def test_suite():
last_accessed = makeSuite(TestLastAccessed, 'test')
start_end = makeSuite(TestNotifications, 'test')
suite = TestSuite((start_end, last_accessed))
return suite
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
import transaction
from unittest import TestCase, makeSuite
from Products.Transience.TransactionHelper import PreventTransactionCommit, \
makeTransactionUncommittable
class TestTransactionHelper(TestCase):
def setUp(self):
self.t = transaction.get()
def tearDown(self):
self.t = None
def testUncommittable(self):
makeTransactionUncommittable(self.t, "test")
self.assertRaises(PreventTransactionCommit, transaction.commit)
transaction.abort()
def test_suite():
suite = makeSuite(TestTransactionHelper, 'test')
return suite
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
from Products.Transience.Transience import TransientObjectContainer
import Products.Transience.TransientObject
import Products.Transience.Transience
from unittest import TestCase, TestSuite, makeSuite
import time as oldtime
import fauxtime
class TestTransientObject(TestCase):
def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
Products.Transience.Transience.setStrict(1)
self.errmargin = .20
self.timeout = fauxtime.timeout
self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60)
def tearDown(self):
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
Products.Transience.Transience.setStrict(0)
self.t = None
del self.t
def test_id(self):
t = self.t.new('xyzzy')
self.assertNotEqual(t.getId(), 'xyzzy') # dont acquire
self.assertEqual(t.getContainerKey(), 'xyzzy')
def test_validate(self):
t = self.t.new('xyzzy')
self.assert_(t.isValid())
t.invalidate()
self.assertFalse(t.isValid())
def test_getLastAccessed(self):
t = self.t.new('xyzzy')
ft = fauxtime.time()
self.assert_(t.getLastAccessed() <= ft)
def test_getCreated(self):
t = self.t.new('xyzzy')
ft = fauxtime.time()
self.assert_(t.getCreated() <= ft)
def test_getLastModifiedUnset(self):
t = self.t.new('xyzzy')
self.assertEqual(t.getLastModified(), None)
def test_getLastModifiedSet(self):
t = self.t.new('xyzzy')
t['a'] = 1
self.assertNotEqual(t.getLastModified(), None)
def testSetLastModified(self):
t = self.t.new('xyzzy')
t.setLastModified()
self.assertNotEqual(t.getLastModified(), None)
def test_setLastAccessed(self):
t = self.t.new('xyzzy')
ft = fauxtime.time()
self.assert_(t.getLastAccessed() <= ft)
fauxtime.sleep(self.timeout * 2) # go to sleep past the granularity
ft2 = fauxtime.time()
t.setLastAccessed()
ft3 = fauxtime.time()
self.assert_(t.getLastAccessed() <= ft3)
self.assert_(t.getLastAccessed() >= ft2)
def _genKeyError(self, t):
return t.get('foobie')
def _genLenError(self, t):
return t.len()
def test_dictionaryLike(self):
t = self.t.new('keytest')
t.update(data)
self.assertEqual(t.keys(), data.keys())
self.assertEqual(t.values(), data.values())
self.assertEqual(t.items(), data.items())
for k in data.keys():
self.assertEqual(t.get(k), data.get(k))
self.assertEqual(t.get('foobie'), None)
self.assertRaises(AttributeError, self._genLenError, t)
self.assertEqual(t.get('foobie',None), None)
self.assert_(t.has_key('a'))
self.assertFalse(t.has_key('foobie'))
t.clear()
self.assertEqual(len(t.keys()), 0)
def test_TTWDictionary(self):
t = self.t.new('mouthfultest')
t.set('foo', 'bar')
self.assertEqual(t['foo'], 'bar')
self.assertEqual(t.get('foo'), 'bar')
t.set('foobie', 'blech')
t.delete('foobie')
self.assertEqual(t.get('foobie'), None)
def test_repr_leaking_information(self):
# __repr__ used to show all contents, which could lead to sensitive
# information being visible in e.g. the ErrorLog object.
t = self.t.new('password-storing-session')
t.set('__ac_password__', 'secret')
self.assertFalse( repr(t).find('secret') != -1
, '__repr__ leaks: %s' % repr(t)
)
def test_suite():
testsuite = makeSuite(TestTransientObject, 'test')
alltests = TestSuite((testsuite,))
return alltests
data = {
'a': 'a',
1: 1,
'Mary': 'no little lamb for you today!',
'epoch': 999999999,
'fauxtime': fauxtime
}
##############################################################################
#
# Copyright (c) 2002 Zope Foundation and Contributors.
#
# 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
#
##############################################################################
import random
from Products.Transience.Transience import TransientObjectContainer,\
MaxTransientObjectsExceeded, SPARE_BUCKETS
from Products.Transience.TransientObject import TransientObject
import Products.Transience.Transience
import Products.Transience.TransientObject
from unittest import TestCase, TestSuite, makeSuite
import time as oldtime
import fauxtime
import slowfauxtime
class TestTransientObjectContainer(TestCase):
def setUp(self):
Products.Transience.Transience.time = fauxtime
Products.Transience.TransientObject.time = fauxtime
Products.Transience.Transience.setStrict(1)
self.errmargin = .20
self.timeout = fauxtime.timeout
self.period = 20
self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60,
period_secs=self.period)
def tearDown(self):
self.t = None
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
Products.Transience.Transience.setStrict(0)
def testGetItemFails(self):
self.assertRaises(KeyError, self._getitemfail)
def _getitemfail(self):
return self.t[10]
def testGetReturnsDefault(self):
self.assertEqual(self.t.get(10), None)
self.assertEqual(self.t.get(10, 'foo'), 'foo')
def testSetItemGetItemWorks(self):
self.t[10] = 1
a = self.t[10]
self.assertEqual(a, 1)
def testReplaceWorks(self):
self.t[10] = 1
self.assertEqual(self.t[10], 1)
self.t[10] = 2
self.assertEqual(self.t[10], 2)
def testHasKeyWorks(self):
self.t[10] = 1
self.assertTrue(self.t.has_key(10))
def testValuesWorks(self):
for x in range(10, 110):
self.t[x] = x
v = self.t.values()
v.sort()
self.assertEqual(len(v), 100)
i = 10
for x in v:
assert x == i
i = i + 1
def testKeysWorks(self):
for x in range(10, 110):
self.t[x] = x
v = self.t.keys()
v.sort()
self.assertEqual(len(v), 100)
i = 10
for x in v:
self.assertEqual(x, i)
i = i + 1
def testItemsWorks(self):
for x in range(10, 110):
self.t[x] = x
v = self.t.items()
v.sort()
self.assertEquals(len(v), 100)
i = 10
for x in v:
self.assertEqual(x[0], i)
self.assertEqual(x[1], i)
i = i + 1
def testDeleteInvalidKeyRaisesKeyError(self):
self.assertRaises(KeyError, self._deletefail)
def _deletefail(self):
del self.t[10]
def testRandomNonOverlappingInserts(self):
added = {}
r = range(10, 110)
for x in r:
k = random.choice(r)
if not added.has_key(k):
self.t[k] = x
added[k] = 1
addl = added.keys()
addl.sort()
self.assertEqual(lsubtract(self.t.keys(),addl), [])
def testRandomOverlappingInserts(self):
added = {}
r = range(10, 110)
for x in r:
k = random.choice(r)
self.t[k] = x
added[k] = 1
addl = added.keys()
addl.sort()
self.assertEqual(lsubtract(self.t.keys(), addl), [])
def testRandomDeletes(self):
r = range(10, 1010)
added = []
for x in r:
k = random.choice(r)
self.t[k] = x
added.append(k)
deleted = []
for x in r:
k = random.choice(r)
if self.t.has_key(k):
del self.t[k]
deleted.append(k)
if self.t.has_key(k):
print "had problems deleting %s" % k
badones = []
for x in deleted:
if self.t.has_key(x):
badones.append(x)
self.assertEqual(badones, [])
def testTargetedDeletes(self):
r = range(10, 1010)
seen = {}
for x in r:
k = random.choice(r)
vals = seen.setdefault(k, [])
vals.append(x)
self.t[k] = x
couldntdelete = {}
weird = []
results = {}
for x in r:
try:
ts, item = self.t.__delitem__(x)
results[x] = ts, item
except KeyError, v:
if v.args[0] != x:
weird.append(x)
couldntdelete[x] = v.args[0]
self.assertEqual(self.t.keys(), [])
def testPathologicalRightBranching(self):
r = range(10, 1010)
for x in r:
self.t[x] = 1
assert list(self.t.keys()) == r, (self.t.keys(), r)
map(self.t.__delitem__, r)
self.assertEqual(list(self.t.keys()), [])
def testPathologicalLeftBranching(self):
r = range(10, 1010)
revr = r[:]
revr.reverse()
for x in revr:
self.t[x] = 1
self.assertEqual(list(self.t.keys()), r)
map(self.t.__delitem__, revr)
self.assertEqual(list(self.t.keys()), [])
def testSuccessorChildParentRewriteExerciseCase(self):
add_order = [
85, 73, 165, 273, 215, 142, 233, 67, 86, 166, 235, 225, 255,
73, 175, 171, 285, 162, 108, 28, 283, 258, 232, 199, 260,
298, 275, 44, 261, 291, 4, 181, 285, 289, 216, 212, 129,
243, 97, 48, 48, 159, 22, 285, 92, 110, 27, 55, 202, 294,
113, 251, 193, 290, 55, 58, 239, 71, 4, 75, 129, 91, 111,
271, 101, 289, 194, 218, 77, 142, 94, 100, 115, 101, 226,
17, 94, 56, 18, 163, 93, 199, 286, 213, 126, 240, 245, 190,
195, 204, 100, 199, 161, 292, 202, 48, 165, 6, 173, 40, 218,
271, 228, 7, 166, 173, 138, 93, 22, 140, 41, 234, 17, 249,
215, 12, 292, 246, 272, 260, 140, 58, 2, 91, 246, 189, 116,
72, 259, 34, 120, 263, 168, 298, 118, 18, 28, 299, 192, 252,
112, 60, 277, 273, 286, 15, 263, 141, 241, 172, 255, 52, 89,
127, 119, 255, 184, 213, 44, 116, 231, 173, 298, 178, 196,
89, 184, 289, 98, 216, 115, 35, 132, 278, 238, 20, 241, 128,
179, 159, 107, 206, 194, 31, 260, 122, 56, 144, 118, 283,
183, 215, 214, 87, 33, 205, 183, 212, 221, 216, 296, 40,
108, 45, 188, 139, 38, 256, 276, 114, 270, 112, 214, 191,
147, 111, 299, 107, 101, 43, 84, 127, 67, 205, 251, 38, 91,
297, 26, 165, 187, 19, 6, 73, 4, 176, 195, 90, 71, 30, 82,
139, 210, 8, 41, 253, 127, 190, 102, 280, 26, 233, 32, 257,
194, 263, 203, 190, 111, 218, 199, 29, 81, 207, 18, 180,
157, 172, 192, 135, 163, 275, 74, 296, 298, 265, 105, 191,
282, 277, 83, 188, 144, 259, 6, 173, 81, 107, 292, 231,
129, 65, 161, 113, 103, 136, 255, 285, 289, 1
]
delete_order = [
276, 273, 12, 275, 2, 286, 127, 83, 92, 33, 101, 195,
299, 191, 22, 232, 291, 226, 110, 94, 257, 233, 215, 184,
35, 178, 18, 74, 296, 210, 298, 81, 265, 175, 116, 261,
212, 277, 260, 234, 6, 129, 31, 4, 235, 249, 34, 289, 105,
259, 91, 93, 119, 7, 183, 240, 41, 253, 290, 136, 75, 292,
67, 112, 111, 256, 163, 38, 126, 139, 98, 56, 282, 60, 26,
55, 245, 225, 32, 52, 40, 271, 29, 252, 239, 89, 87, 205,
213, 180, 97, 108, 120, 218, 44, 187, 196, 251, 202, 203,
172, 28, 188, 77, 90, 199, 297, 282, 141, 100, 161, 216,
73, 19, 17, 189, 30, 258
]
for x in add_order:
self.t[x] = 1
for x in delete_order:
try: del self.t[x]
except KeyError:
self.assertFalse(self.t.has_key(x))
def testGetDelaysTimeout(self):
for x in range(10, 110):
self.t[x] = x
# current bucket will become old after we sleep for a while.
fauxtime.sleep(self.timeout/2)
# these items will be added to the new current bucket by getitem
for x in range(10, 110):
self.t.get(x)
fauxtime.sleep(self.timeout/2)
self.assertEqual(len(self.t.keys()), 100)
for x in range(10, 110):
self.assertEqual(self.t[x], x)
def testSetItemDelaysTimeout(self):
for x in range(10, 110):
self.t[x] = x
# current bucket will become old after we sleep for a while.
fauxtime.sleep(self.timeout/2)
# these items will be added to the new current bucket by setitem
for x in range(10, 110):
self.t[x] = x + 1
fauxtime.sleep(self.timeout/2)
assert len(self.t.keys()) == 100, len(self.t.keys())
for x in range(10, 110):
assert self.t[x] == x + 1
def testLen(self):
# This test must not time out else it will fail.
# make timeout extremely unlikely by setting it very high
self.t._setTimeout(self.timeout, self.period)
added = {}
r = range(10, 1010)
for x in r:
k = random.choice(r)
self.t[k] = x
added[k] = x
self.assertEqual(len(self.t), len(added))
for k in added.keys():
del self.t[k]
self.assertEqual(len(self.t), 0)
def testResetWorks(self):
self.t[10] = 1
self.t._reset()
self.assertFalse(self.t.get(10))
def testGetTimeoutMinutesWorks(self):
self.assertEqual(self.t.getTimeoutMinutes(), self.timeout / 60)
self.t._setTimeout(10, 30)
self.assertEqual(self.t.getTimeoutMinutes(), 10)
self.assertEqual(self.t.getPeriodSeconds(), 30)
def test_new(self):
t = self.t.new('foobieblech')
self.assertTrue(issubclass(t.__class__, TransientObject))
def _dupNewItem(self):
self.t.new('foobieblech')
def test_newDupFails(self):
self.t.new('foobieblech')
self.assertRaises(KeyError, self._dupNewItem)
def test_new_or_existing(self):
t = self.t.new('foobieblech')
t['hello'] = "Here I am!"
t2 = self.t.new_or_existing('foobieblech')
self.assertEqual(t2['hello'], "Here I am!")
def test_getId(self):
self.assertEqual(self.t.getId(), 'sdc')
def testSubobjectLimitWorks(self):
self.t = TransientObjectContainer('a', timeout_mins=self.timeout/60,
limit=10)
self.assertRaises(MaxTransientObjectsExceeded, self._maxOut)
def testZeroTimeoutMeansPersistForever(self):
self.t._setTimeout(0, self.period)
self.t._reset()
for x in range(10, 110):
self.t[x] = x
fauxtime.sleep(180)
self.assertEqual(len(self.t.keys()), 100)
def testGarbageCollection(self):
# this is pretty implementation-dependent :-(
for x in range(0, 100):
self.t[x] = x
sleeptime = self.period * SPARE_BUCKETS
fauxtime.sleep(sleeptime)
self.t._invoke_finalize_and_gc()
max_ts = self.t._last_finalized_timeslice()
keys = list(self.t._data.keys())
for k in keys:
self.assert_(k > max_ts, "k %s < max_ts %s" % (k, max_ts))
def _maxOut(self):
for x in range(11):
self.t.new(str(x))
class TestSlowTransientObjectContainer(TestCase):
def setUp(self):
Products.Transience.Transience.time = slowfauxtime
Products.Transience.TransientObject.time = slowfauxtime
Products.Transience.Transience.setStrict(1)
self.errmargin = .20
self.timeout = 120
self.period = 20
self.t = TransientObjectContainer('sdc', timeout_mins=self.timeout/60,
period_secs=self.period)
def tearDown(self):
self.t = None
Products.Transience.Transience.time = oldtime
Products.Transience.TransientObject.time = oldtime
Products.Transience.Transience.setStrict(0)
def testChangingTimeoutWorks(self):
# TODO: This test is slooooow
# 1 minute
for x in range(10, 110):
self.t[x] = x
slowfauxtime.sleep(self.timeout * (self.errmargin + 1))
self.assertEqual(len(self.t.keys()), 0)
# 2 minutes
self.t._setTimeout(self.timeout/60*2, self.period)
self.t._reset()
for x in range(10, 110):
self.t[x] = x
slowfauxtime.sleep(self.timeout)
self.assertEqual(len(self.t.keys()), 100)
slowfauxtime.sleep(self.timeout * (self.errmargin+1))
self.assertEqual(len(self.t.keys()), 0)
# 3 minutes
self.t._setTimeout(self.timeout/60*3, self.period)
self.t._reset()
for x in range(10, 110):
self.t[x] = x
slowfauxtime.sleep(self.timeout)
self.assertEqual(len(self.t.keys()), 100)
slowfauxtime.sleep(self.timeout)
self.assertEqual(len(self.t.keys()), 100)
slowfauxtime.sleep(self.timeout * (self.errmargin+1))
self.assertEqual(len(self.t.keys()), 0)
def testItemsGetExpired(self):
for x in range(10, 110):
self.t[x] = x
# these items will time out while we sleep
slowfauxtime.sleep(self.timeout * (self.errmargin+1))
for x in range(110, 210):
self.t[x] = x
self.assertEqual(len(self.t.keys()), 100)
# call _gc just to make sure __len__ gets changed after a gc
#self.t._gc()
self.assertEqual(len(self.t), 100)
# we should still have 100 - 199
for x in range(110, 210):
self.assertEqual(self.t[x], x)
# but we shouldn't have 0 - 100
for x in range(10, 110):
try: self.t[x]
except KeyError: pass
else: assert 1 == 2, x
def lsubtract(l1, l2):
l1=list(l1)
l2=list(l2)
l = filter(lambda x, l1=l1: x not in l1, l2)
l = l + filter(lambda x, l2=l2: x not in l2, l1)
return l
def test_suite():
suite = TestSuite()
suite.addTest(makeSuite(TestTransientObjectContainer))
suite.addTest(makeSuite(TestSlowTransientObjectContainer))
return suite
......@@ -20,6 +20,7 @@ Products.BTreeFolder2 = 3.0
Products.ExternalMethod = 3.0
Products.MailHost = 3.0
Products.PythonScripts = 3.0
Products.Sessions = 4.0
Products.SiteErrorLog = 4.0
Products.StandardCacheManagers = 3.0
Products.ZCatalog = 4.0a1
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment