-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move webdav's EtagSupport, Lockable and LockItem into OFS.
- Loading branch information
Showing
31 changed files
with
1,338 additions
and
947 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.