############################################################################## # # Copyright (c) 2002 Zope Corporation and Contributors. All Rights Reserved. # # This software is subject to the provisions of the Zope Public License, # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS # FOR A PARTICULAR PURPOSE. # ############################################################################## """WebDAV support - null resource objects. $Id: NullResource.py 67823 2006-05-02 09:55:17Z philikon $ """ import sys import Acquisition import OFS.SimpleItem from Globals import InitializeClass from AccessControl import getSecurityManager from AccessControl import ClassSecurityInfo from AccessControl.Permissions import view as View from AccessControl.Permissions import add_folders from AccessControl.Permissions import webdav_lock_items from AccessControl.Permissions import webdav_unlock_items from Globals import Persistent, DTMLFile from OFS.CopySupport import CopyError from zExceptions import MethodNotAllowed from zExceptions import Unauthorized, NotFound, Forbidden, BadRequest import davcmds from common import aq_base, tokenFinder, IfParser from common import isDavCollection from common import Locked, Conflict, PreconditionFailed, UnsupportedMediaType from interfaces import IWriteLock from Resource import Resource from WriteLockInterface import WriteLockInterface from zope.contenttype import guess_content_type class NullResource(Persistent, Acquisition.Implicit, Resource): """Null resources are used to handle HTTP method calls on objects which do not yet exist in the url namespace.""" __implements__ = (WriteLockInterface,) __null_resource__=1 security = ClassSecurityInfo() def __init__(self, parent, name, request=None): self.__name__=name self.__parent__=parent def __bobo_traverse__(self, REQUEST, name=None): # We must handle traversal so that we can recognize situations # where a 409 Conflict must be returned instead of the normal # 404 Not Found, per [WebDAV 8.3.1]. try: return getattr(self, name) except: pass method=REQUEST.get('REQUEST_METHOD', 'GET') if method in ('PUT', 'MKCOL', 'LOCK'): raise Conflict, 'Collection ancestors must already exist.' raise NotFound, 'The requested resource was not found.' security.declareProtected(View, 'HEAD') def HEAD(self, REQUEST, RESPONSE): """Retrieve resource information without a response message body.""" self.dav__init(REQUEST, RESPONSE) raise NotFound, 'The requested resource does not exist.' # Most methods return 404 (Not Found) for null resources. DELETE=TRACE=PROPFIND=PROPPATCH=COPY=MOVE=HEAD index_html = HEAD def _default_PUT_factory( self, name, typ, body ): # Return DTMLDoc/PageTemplate/Image/File, based on sniffing. if name and name.endswith('.pt'): from Products.PageTemplates.ZopePageTemplate import ZopePageTemplate ob = ZopePageTemplate(name, body, content_type=typ) elif typ in ('text/html', 'text/xml', 'text/plain'): from OFS.DTMLDocument import DTMLDocument ob = DTMLDocument( '', __name__=name ) elif typ[:6]=='image/': from OFS.Image import Image ob=Image(name, '', body, content_type=typ) else: from OFS.Image import File ob=File(name, '', body, content_type=typ) return ob security.declarePublic('PUT') def PUT(self, REQUEST, RESPONSE): """Create a new non-collection resource. """ from ZServer import LARGE_FILE_THRESHOLD self.dav__init(REQUEST, RESPONSE) name = self.__name__ parent = self.__parent__ ifhdr = REQUEST.get_header('If', '') if (IWriteLock.providedBy(parent) or WriteLockInterface.isImplementedBy(parent)) and \ parent.wl_isLocked(): if ifhdr: parent.dav__simpleifhandler(REQUEST, RESPONSE, col=1) else: # There was no If header at all, and our parent is locked, # so we fail here raise Locked elif ifhdr: # There was an If header, but the parent is not locked raise PreconditionFailed # SDS: Only use BODY if the file size is smaller than # LARGE_FILE_THRESHOLD, otherwise read LARGE_FILE_THRESHOLD # bytes from the file which should be enough to trigger # content_type detection, and possibly enough for CMF's # content_type_registry too. # # Note that body here is really just used for detecting the # content type and figuring out the correct factory. The correct # file content will be uploaded on ob.PUT(REQUEST, RESPONSE) after # the object has been created. # # A problem I could see is content_type_registry predicates # that do depend on the whole file being passed here as an # argument. There's none by default that does this though. If # they really do want to look at the file, they should use # REQUEST['BODYFILE'] directly and try as much as possible not # to read the whole file into memory. if int(REQUEST.get('CONTENT_LENGTH') or 0) > LARGE_FILE_THRESHOLD: file = REQUEST['BODYFILE'] body = file.read(LARGE_FILE_THRESHOLD) file.seek(0) else: body = REQUEST.get('BODY', '') typ=REQUEST.get_header('content-type', None) if typ is None: typ, enc=guess_content_type(name, body) factory = getattr(parent, 'PUT_factory', self._default_PUT_factory ) ob = factory(name, typ, body) if ob is None: ob = self._default_PUT_factory(name, typ, body) # We call _verifyObjectPaste with verify_src=0, to see if the # user can create this type of object (and we don't need to # check the clipboard. try: parent._verifyObjectPaste(ob.__of__(parent), 0) except CopyError: raise Unauthorized, sys.exc_info()[1] # Delegate actual PUT handling to the new object, # SDS: But just *after* it has been stored. self.__parent__._setObject(name, ob) ob = self.__parent__._getOb(name) ob.PUT(REQUEST, RESPONSE) RESPONSE.setStatus(201) RESPONSE.setBody('') return RESPONSE security.declareProtected(add_folders, 'MKCOL') def MKCOL(self, REQUEST, RESPONSE): """Create a new collection resource.""" self.dav__init(REQUEST, RESPONSE) if REQUEST.get('BODY', ''): raise UnsupportedMediaType, 'Unknown request body.' name=self.__name__ parent = self.__parent__ if hasattr(aq_base(parent), name): raise MethodNotAllowed, 'The name %s is in use.' % name if not isDavCollection(parent): raise Forbidden, 'Cannot create collection at this location.' ifhdr = REQUEST.get_header('If', '') if (IWriteLock.providedBy(parent) or WriteLockInterface.isImplementedBy(parent)) and \ parent.wl_isLocked(): if ifhdr: parent.dav__simpleifhandler(REQUEST, RESPONSE, col=1) else: raise Locked elif ifhdr: # There was an If header, but the parent is not locked raise PreconditionFailed # Add hook for webdav/FTP MKCOL (Collector #2254) (needed for CMF) # parent.manage_addFolder(name) mkcol_handler = getattr(parent,'MKCOL_handler' ,parent.manage_addFolder) mkcol_handler(name) RESPONSE.setStatus(201) RESPONSE.setBody('') return RESPONSE security.declareProtected(webdav_lock_items, 'LOCK') def LOCK(self, REQUEST, RESPONSE): """ LOCK on a Null Resource makes a LockNullResource instance """ self.dav__init(REQUEST, RESPONSE) security = getSecurityManager() creator = security.getUser() body = REQUEST.get('BODY', '') ifhdr = REQUEST.get_header('If', '') depth = REQUEST.get_header('Depth', 'infinity') name = self.__name__ parent = self.__parent__ if (IWriteLock.providedBy(parent) or WriteLockInterface.isImplementedBy(parent)) and \ parent.wl_isLocked(): if ifhdr: parent.dav__simpleifhandler(REQUEST, RESPONSE, col=1) else: raise Locked elif ifhdr: # There was an If header, but the parent is not locked. raise PreconditionFailed # The logic involved in locking a null resource is simpler than # a regular resource, since we know we're not already locked, # and the lock isn't being refreshed. if not body: raise BadRequest, 'No body was in the request' locknull = LockNullResource(name) parent._setObject(name, locknull) locknull = parent._getOb(name) cmd = davcmds.Lock(REQUEST) token, result = cmd.apply(locknull, creator, depth=depth) if result: # Return the multistatus result (there were multiple errors) # This *shouldn't* happen for locking a NullResource, but it's # inexpensive to handle and is good coverage for any future # changes in davcmds.Lock RESPONSE.setStatus(207) RESPONSE.setHeader('Content-Type', 'text/xml; charset="utf-8"') RESPONSE.setBody(result) else: # The command was succesful lock = locknull.wl_getLock(token) RESPONSE.setStatus(200) RESPONSE.setHeader('Content-Type', 'text/xml; charset="utf-8"') RESPONSE.setHeader('Lock-Token', 'opaquelocktoken:' + token) RESPONSE.setBody(lock.asXML()) InitializeClass(NullResource) class LockNullResource(NullResource, OFS.SimpleItem.Item_w__name__): """ A Lock-Null Resource is created when a LOCK command is succesfully executed on a NullResource, essentially locking the Name. A PUT or MKCOL deletes the LockNull resource from its container and replaces it with the target object. An UNLOCK deletes it. """ __implements__ = (WriteLockInterface,) __locknull_resource__ = 1 meta_type = 'WebDAV LockNull Resource' security = ClassSecurityInfo() manage_options = ({'label': 'Info', 'action': 'manage_main'},) security.declareProtected(View, 'manage') security.declareProtected(View, 'manage_main') manage = manage_main = DTMLFile('dtml/locknullmain', globals()) security.declareProtected(View, 'manage_workspace') manage_workspace = manage manage_main._setName('manage_main') # explicit def __no_valid_write_locks__(self): # A special hook (for better or worse) called when there are no # valid locks left. We have to delete ourselves from our container # now. parent = Acquisition.aq_parent(self) if parent: parent._delObject(self.id) def __init__(self, name): self.id = self.__name__ = name self.title = "LockNull Resource '%s'" % name security.declarePublic('title_or_id') def title_or_id(self): return 'Foo' def PROPFIND(self, REQUEST, RESPONSE): """Retrieve properties defined on the resource.""" return Resource.PROPFIND(self, REQUEST, RESPONSE) security.declareProtected(webdav_lock_items, 'LOCK') def LOCK(self, REQUEST, RESPONSE): """ A Lock command on a LockNull resource should only be a refresh request (one without a body) """ self.dav__init(REQUEST, RESPONSE) body = REQUEST.get('BODY', '') ifhdr = REQUEST.get_header('If', '') if body: # If there's a body, then this is a full lock request # which conflicts with the fact that we're already locked RESPONSE.setStatus(423) else: # There's no body, so this is likely to be a refresh request if not ifhdr: raise PreconditionFailed taglist = IfParser(ifhdr) found = 0 for tag in taglist: for listitem in tag.list: token = tokenFinder(listitem) if token and self.wl_hasLock(token): lock = self.wl_getLock(token) timeout = REQUEST.get_header('Timeout', 'infinite') lock.setTimeout(timeout) # Automatically refreshes found = 1 RESPONSE.setStatus(200) RESPONSE.setHeader('Content-Type', 'text/xml; charset="utf-8"') RESPONSE.setBody(lock.asXML()) if found: break if not found: RESPONSE.setStatus(412) # Precondition failed return RESPONSE security.declareProtected(webdav_unlock_items, 'UNLOCK') def UNLOCK(self, REQUEST, RESPONSE): """ Unlocking a Null Resource removes it from its parent """ self.dav__init(REQUEST, RESPONSE) security = getSecurityManager() user = security.getUser() token = REQUEST.get_header('Lock-Token', '') url = REQUEST['URL'] if token: token = tokenFinder(token) else: raise BadRequest, 'No lock token was submitted in the request' cmd = davcmds.Unlock() result = cmd.apply(self, token, url) parent = Acquisition.aq_parent(self) parent._delObject(self.id) if result: RESPONSE.setStatus(207) RESPONSE.setHeader('Content-Type', 'text/xml; charset="utf-8"') RESPONSE.setBody(result) else: RESPONSE.setStatus(204) return RESPONSE security.declarePublic('PUT') def PUT(self, REQUEST, RESPONSE): """ Create a new non-collection resource, deleting the LockNull object from the container before putting the new object in. """ self.dav__init(REQUEST, RESPONSE) name = self.__name__ parent = self.aq_parent parenturl = parent.absolute_url() ifhdr = REQUEST.get_header('If', '') # Since a Lock null resource is always locked by definition, all # operations done by an owner of the lock that affect the resource # MUST have the If header in the request if not ifhdr: raise PreconditionFailed, 'No If-header' # First we need to see if the parent of the locknull is locked, and # if the user owns that lock (checked by handling the information in # the If header). if (IWriteLock.providedBy(parent) or WriteLockInterface.isImplementedBy(parent)) and \ parent.wl_isLocked(): itrue = parent.dav__simpleifhandler(REQUEST, RESPONSE, 'PUT', col=1, url=parenturl, refresh=1) if not itrue: raise PreconditionFailed, ( 'Condition failed against resources parent') # Now we need to check the If header against our own lock state itrue = self.dav__simpleifhandler(REQUEST, RESPONSE, 'PUT', refresh=1) if not itrue: raise PreconditionFailed, ( 'Condition failed against locknull resource') # All of the If header tests succeeded, now we need to remove ourselves # from our parent. We need to transfer lock state to the new object. locks = self.wl_lockItems() parent._delObject(name) # Now we need to go through the regular operations of PUT body = REQUEST.get('BODY', '') typ = REQUEST.get_header('content-type', None) if typ is None: typ, enc = guess_content_type(name, body) factory = getattr(parent, 'PUT_factory', self._default_PUT_factory) ob = (factory(name, typ, body) or self._default_PUT_factory(name, typ, body)) # Verify that the user can create this type of object try: parent._verifyObjectPaste(ob.__of__(parent), 0) except Unauthorized: raise except: raise Forbidden, sys.exc_info()[1] # Put the locks on the new object if not (IWriteLock.providedBy(ob) or WriteLockInterface.isImplementedBy(ob)): raise MethodNotAllowed, ( 'The target object type cannot be locked') for token, lock in locks: ob.wl_setLock(token, lock) # Delegate actual PUT handling to the new object. ob.PUT(REQUEST, RESPONSE) parent._setObject(name, ob) RESPONSE.setStatus(201) RESPONSE.setBody('') return RESPONSE security.declareProtected(add_folders, 'MKCOL') def MKCOL(self, REQUEST, RESPONSE): """ Create a new Collection (folder) resource. Since this is being done on a LockNull resource, this also involves removing the LockNull object and transferring its locks to the newly created Folder """ self.dav__init(REQUEST, RESPONSE) if REQUEST.get('BODY', ''): raise UnsupportedMediaType, 'Unknown request body.' name = self.__name__ parent = self.aq_parent parenturl = parent.absolute_url() ifhdr = REQUEST.get_header('If', '') if not ifhdr: raise PreconditionFailed, 'No If-header' # If the parent object is locked, that information should be in the # if-header if the user owns a lock on the parent if (IWriteLock.providedBy(parent) or WriteLockInterface.isImplementedBy(parent)) and \ parent.wl_isLocked(): itrue = parent.dav__simpleifhandler(REQUEST, RESPONSE, 'MKCOL', col=1, url=parenturl, refresh=1) if not itrue: raise PreconditionFailed, ( 'Condition failed against resources parent') # Now we need to check the If header against our own lock state itrue = self.dav__simpleifhandler(REQUEST,RESPONSE,'MKCOL',refresh=1) if not itrue: raise PreconditionFailed, ( 'Condition failed against locknull resource') # All of the If header tests succeeded, now we need to remove ourselves # from our parent. We need to transfer lock state to the new folder. locks = self.wl_lockItems() parent._delObject(name) parent.manage_addFolder(name) folder = parent._getOb(name) for token, lock in locks: folder.wl_setLock(token, lock) RESPONSE.setStatus(201) RESPONSE.setBody('') return RESPONSE InitializeClass(LockNullResource)