Skip to content

Commit

Permalink
Move webdav's EtagSupport, Lockable and LockItem into OFS.
Browse files Browse the repository at this point in the history
  • Loading branch information
hannosch committed Aug 6, 2016
1 parent 32e8dea commit d1536a5
Show file tree
Hide file tree
Showing 31 changed files with 1,338 additions and 947 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ Features Added

- AccessControl = 4.0a3
- AuthEncoding = 4.0.0
- Products.PythonScripts = 4.0
- zExceptions = 3.3

Restructuring
+++++++++++++

- Move webdav's EtagSupport, Lockable and LockItem into OFS.

- Split `Products.TemporaryFolder` and `Products.ZODBMountPoint` into
one new project called `Products.TemporaryFolder`.

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Persistence==3.0a1
Products.BTreeFolder2==3.0
Products.ExternalMethod==3.0
Products.MailHost==3.0
Products.PythonScripts==3.0
Products.PythonScripts==4.0
Products.Sessions==4.0
Products.SiteErrorLog==4.0
Products.StandardCacheManagers==3.0
Expand Down
142 changes: 142 additions & 0 deletions src/OFS/EtagSupport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
##############################################################################
#
# 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 zope.interface import implements
from zope.interface import Interface

from zExceptions import HTTPPreconditionFailed


class EtagBaseInterface(Interface):
"""\
Basic Etag support interface, meaning the object supports generating
an Etag that can be used by certain HTTP and WebDAV Requests.
"""
def http__etag():
"""\
Entity tags are used for comparing two or more entities from
the same requested resource. Predominantly used for Caching,
Etags can also be used to deal with the 'Lost Updates Problem'.
An HTTP Client such as Amaya that supports PUT for editing can
use the Etag value returned in the head of a GET response in the
'if-match' header submitted with a PUT request. If the Etag
for the requested resource in the PUT request's 'if-match' header
is different from the current Etag value returned by this method,
the PUT will fail (it means that the state of the resource has
changed since the last copy the Client recieved) because the
precondition (the 'if-match') fails (the submitted Etag does not
match the current Etag).
"""

def http__refreshEtag():
"""\
While it may make sense to use the ZODB Object Id or the
database mtime to generate an Etag, this could
fail on certain REQUESTS because:
o The object is not stored in the ZODB, or
o A Request such as PUT changes the oid or database mtime
*AFTER* the Response has been written out, but the Etag needs
to be updated and returned with the Response of the PUT request.
Thus, Etags need to be refreshed manually when an object changes.
"""


class EtagSupport(object):
"""
This class is the basis for supporting Etags in Zope. It's main
function right now is to support the *Lost Updates Problem* by
allowing Etags and If-Match headers to be checked on PUT calls to
provide a *Seatbelt* style functionality. The Etags is based on
the databaes mtime, and thus is updated whenever the
object is updated. If a PUT request, or other HTTP or Dav request
comes in with an Etag different than the current one, that request
can be rejected according to the type of header (If-Match,
If-None-Match).
"""
implements(EtagBaseInterface)

def http__etag(self, readonly=0):
try:
etag = self.__etag
except AttributeError:
if readonly: # Don't refresh the etag on reads
return
self.http__refreshEtag()
etag = self.__etag
return etag

def http__refreshEtag(self):
self.__etag = 'ts%s' % str(time.time())[2:]

def http__parseMatchList(self, REQUEST, header="if-match"):
# Return a sequence of strings found in the header specified
# (should be one of {'if-match' or 'if-none-match'}). If the
# header is not in the request, returns None. Otherwise,
# returns a tuple of Etags.
matchlist = REQUEST.get_header(header)
if matchlist is None:
matchlist = REQUEST.get_header(header.title())
if matchlist is None:
return None
matchlist = [x.strip() for x in matchlist.split(',')]

r = []
for match in matchlist:
if match == '*':
r.insert(0, match)
elif (match[0] + match[-1] == '""') and (len(match) > 2):
r.append(match[1:-1])

return tuple(r)

def http__processMatchHeaders(self, REQUEST=None):
# Process if-match and if-none-match headers

if REQUEST is None:
REQUEST = self.aq_acquire('REQUEST')

matchlist = self.http__parseMatchList(REQUEST, 'if-match')
nonematch = self.http__parseMatchList(REQUEST, 'if-none-match')

if matchlist is None:
# There's no Matchlist, but 'if-none-match' might need processing
pass
elif ('*' in matchlist):
return 1 # * matches everything
elif self.http__etag() not in matchlist:
# The resource etag is not in the list of etags required
# to match, as specified in the 'if-match' header. The
# condition fails and the HTTP Method may *not* execute.
raise HTTPPreconditionFailed()
elif self.http__etag() in matchlist:
return 1

if nonematch is None:
# There's no 'if-none-match' header either, so there's no
# problem continuing with the request
return 1
elif ('*' in nonematch):
# if-none-match: * means that the operation should not
# be performed if the specified resource exists
raise HTTPPreconditionFailed()
elif self.http__etag() in nonematch:
# The opposite of if-match, the condition fails
# IF the resources Etag is in the if-none-match list
raise HTTPPreconditionFailed()
elif self.http__etag() not in nonematch:
return 1
15 changes: 13 additions & 2 deletions src/OFS/Folder.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from OFS.FindSupport import FindSupport
from OFS.interfaces import IFolder
from OFS.Lockable import LockableItem
from OFS.ObjectManager import ObjectManager
from OFS.PropertyManager import PropertyManager
from OFS.role import RoleManager
Expand All @@ -30,10 +31,19 @@
from webdav.Collection import Collection
except ImportError:
class Collection(object):
pass
def dav__init(self, request, response):
pass

def dav__validate(self, object, methodname, REQUEST):
pass

def dav__simpleifhandler(self, request, response, method='PUT',
col=0, url=None, refresh=0):
pass


manage_addFolderForm = DTMLFile('dtml/folderAdd', globals())

manage_addFolderForm=DTMLFile('dtml/folderAdd', globals())

def manage_addFolder(self, id, title='',
createPublic=0,
Expand All @@ -54,6 +64,7 @@ class Folder(
PropertyManager,
RoleManager,
Collection,
LockableItem,
Item,
FindSupport,
):
Expand Down
190 changes: 190 additions & 0 deletions src/OFS/LockItem.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
##############################################################################
#
# 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
import time

from AccessControl.class_init import InitializeClass
from AccessControl.owner import ownerInfo
from AccessControl.SecurityInfo import ClassSecurityInfo
from Persistence import Persistent
from zope.interface import implements

from OFS.interfaces import ILockItem

_randGen = random.Random(time.time())
MAXTIMEOUT = (2**32) - 1 # Maximum timeout time
DEFAULTTIMEOUT = 12 * 60 # Default timeout


def generateLockToken():
# Generate a lock token
return '%s-%s-00105A989226:%.03f' % \
(_randGen.random(), _randGen.random(), time.time())


def validateTimeout(timeout):
# Timeout *should* be in the form "Seconds-XXX" or "Infinite"
errors = []
try:
t = str(timeout).split('-')[-1]
if t.lower() == 'infinite':
# Default to 1800 seconds for infinite requests
timeout = DEFAULTTIMEOUT
else:
timeout = int(t)
except ValueError:
errors.append("Bad timeout value")
if timeout > MAXTIMEOUT:
errors.append("Timeout request is greater than %s" % MAXTIMEOUT)
return timeout, errors


class LockItem(Persistent):

implements(ILockItem)

security = ClassSecurityInfo()
security.declarePublic('getOwner', 'getLockToken', 'getDepth',
'getTimeout', 'getTimeoutString',
'getModifiedTime', 'isValid', 'getLockScope',
'getLockType')
security.declareProtected('Change Lock Information',
'setTimeout', 'refresh')
security.declareProtected('Access contents information',
'getCreator', 'getCreatorPath')

def __init__(self, creator, owner='', depth=0, timeout='Infinite',
locktype='write', lockscope='exclusive', token=None):
errors = []
# First check the values and raise value errors if outside of contract
if not getattr(creator, 'getUserName', None):
errors.append("Creator not a user object")
if str(depth).lower() not in ('0', 'infinity'):
errors.append("Depth must be 0 or infinity")
if locktype.lower() != 'write':
errors.append("Lock type '%s' not supported" % locktype)
if lockscope.lower() != 'exclusive':
errors.append("Lock scope '%s' not supported" % lockscope)

timeout, e = validateTimeout(timeout)
errors = errors + e

# Finally, if there were errors, report them ALL to on high
if errors:
raise ValueError(errors)

# AccessControl.owner.ownerInfo returns the id of the creator
# and the path to the UserFolder they're defined in
self._creator = ownerInfo(creator)

self._owner = owner
self._depth = depth
self._timeout = timeout
self._locktype = locktype
self._lockscope = lockscope
self._modifiedtime = time.time()

if token is None:
self._token = generateLockToken()
else:
self._token = token

def getCreator(self):
return self._creator

def getCreatorPath(self):
db, name = self._creator
path = '/'.join(db)
return "/%s/%s" % (path, name)

def getOwner(self):
return self._owner

def getLockToken(self):
return self._token

def getDepth(self):
return self._depth

def getTimeout(self):
return self._timeout

def getTimeoutString(self):
t = str(self._timeout)
if t[-1] == 'L':
t = t[:-1] # lob off Long signifier
return "Second-%s" % t

def setTimeout(self, newtimeout):
timeout, errors = validateTimeout(newtimeout)
if errors:
raise ValueError(errors)
else:
self._timeout = timeout
self._modifiedtime = time.time() # reset modified

def getModifiedTime(self):
return self._modifiedtime

def refresh(self):
self._modifiedtime = time.time()

def isValid(self):
now = time.time()
modified = self._modifiedtime
timeout = self._timeout

return (modified + timeout) > now

def getLockType(self):
return self._locktype

def getLockScope(self):
return self._lockscope

def asLockDiscoveryProperty(self, ns='d', fake=0):
if fake:
token = 'this-is-a-faked-no-permission-token'
else:
token = self._token
s = (' <%(ns)s:activelock>\n'
' <%(ns)s:locktype><%(ns)s:%(locktype)s/></%(ns)s:locktype>\n'
' <%(ns)s:lockscope><%(ns)s:%(lockscope)s/></%(ns)s:lockscope>\n'
' <%(ns)s:depth>%(depth)s</%(ns)s:depth>\n'
' <%(ns)s:owner>%(owner)s</%(ns)s:owner>\n'
' <%(ns)s:timeout>%(timeout)s</%(ns)s:timeout>\n'
' <%(ns)s:locktoken>\n'
' <%(ns)s:href>opaquelocktoken:%(locktoken)s</%(ns)s:href>\n'
' </%(ns)s:locktoken>\n'
' </%(ns)s:activelock>\n'
) % {
'ns': ns,
'locktype': self._locktype,
'lockscope': self._lockscope,
'depth': self._depth,
'owner': self._owner,
'timeout': self.getTimeoutString(),
'locktoken': token}
return s

def asXML(self):
s = """<?xml version="1.0" encoding="utf-8" ?>
<d:prop xmlns:d="DAV:">
<d:lockdiscovery>
%s
</d:lockdiscovery>
</d:prop>""" % self.asLockDiscoveryProperty(ns="d")
return s

InitializeClass(LockItem)
Loading

0 comments on commit d1536a5

Please sign in to comment.