~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/web2/dav/resource.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
##
 
2
# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
 
3
#
 
4
# Permission is hereby granted, free of charge, to any person obtaining a copy
 
5
# of this software and associated documentation files (the "Software"), to deal
 
6
# in the Software without restriction, including without limitation the rights
 
7
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 
8
# copies of the Software, and to permit persons to whom the Software is
 
9
# furnished to do so, subject to the following conditions:
 
10
 
11
# The above copyright notice and this permission notice shall be included in all
 
12
# copies or substantial portions of the Software.
 
13
 
14
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 
15
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 
16
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 
17
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 
18
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 
19
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 
20
# SOFTWARE.
 
21
#
 
22
# DRI: Wilfredo Sanchez, wsanchez@apple.com
 
23
##
 
24
 
 
25
"""
 
26
WebDAV resources.
 
27
"""
 
28
 
 
29
__all__ = [
 
30
    "DAVPropertyMixIn",
 
31
    "DAVResource",
 
32
    "DAVLeafResource"
 
33
]
 
34
 
 
35
import urllib
 
36
 
 
37
from zope.interface import implements
 
38
from twisted.python import log
 
39
from twisted.internet.defer import maybeDeferred, succeed
 
40
from twisted.web2 import responsecode
 
41
from twisted.web2.dav import davxml
 
42
from twisted.web2.dav.davxml import dav_namespace, lookupElement
 
43
from twisted.web2.dav.idav import IDAVResource
 
44
from twisted.web2.dav.noneprops import NonePropertyStore
 
45
from twisted.web2.dav.util import unimplemented
 
46
from twisted.web2.http import HTTPError, RedirectResponse, StatusResponse
 
47
from twisted.web2.http_headers import generateContentType
 
48
from twisted.web2.iweb import IResponse
 
49
from twisted.web2.resource import LeafResource
 
50
from twisted.web2.static import MetaDataMixin, StaticRenderMixin
 
51
 
 
52
twisted_dav_namespace = "http://twistedmatrix.com/xml_namespace/dav/"
 
53
twisted_private_namespace = "http://twistedmatrix.com/xml_namespace/dav/private/"
 
54
 
 
55
class DAVPropertyMixIn (MetaDataMixin):
 
56
    """
 
57
    Mix-in class which implements the DAV property access API in
 
58
    L{IDAVResource}.
 
59
 
 
60
    There are three categories of DAV properties, for the purposes of how this
 
61
    class manages them.  A X{property} is either a X{live property} or a
 
62
    X{dead property}, and live properties are split into two categories:
 
63
    
 
64
    1. Dead properties.  There are properties that the server simply stores as
 
65
       opaque data.  These are store in the X{dead property store}, which is
 
66
       provided by subclasses via the L{deadProperties} method.
 
67
 
 
68
    2. Live properties which are always computed.  These properties aren't
 
69
       stored anywhere (by this class) but instead are derived from the resource
 
70
       state or from data that is persisted elsewhere.  These are listed in the
 
71
       L{liveProperties} attribute and are handled explicitly by the
 
72
       L{readProperty} method.
 
73
 
 
74
    3. Live properties may be acted on specially and are stored in the X{dead
 
75
       property store}.  These are not listed in the L{liveProperties} attribute,
 
76
       but may be handled specially by the property access methods.  For
 
77
       example, L{writeProperty} might validate the data and refuse to write
 
78
       data it deems inappropriate for a given property.
 
79
 
 
80
    There are two sets of property access methods.  The first group
 
81
    (L{hasProperty}, etc.) provides access to all properties.  They
 
82
    automatically figure out which category a property falls into and act
 
83
    accordingly.
 
84
    
 
85
    The second group (L{hasDeadProperty}, etc.) accesses the dead property store
 
86
    directly and bypasses any live property logic that exists in the first group
 
87
    of methods.  These methods are used by the first group of methods, and there
 
88
    are cases where they may be needed by other methods.  I{Accessing dead
 
89
    properties directly should be done with caution.}  Bypassing the live
 
90
    property logic means that values may not be the correct ones for use in
 
91
    DAV requests such as PROPFIND, and may be bypassing security checks.  In
 
92
    general, one should never bypass the live property logic as part of a client
 
93
    request for property data.
 
94
 
 
95
    Properties in the L{twisted_private_namespace} namespace are internal to the
 
96
    server and should not be exposed to clients.  They can only be accessed via
 
97
    the dead property store.
 
98
    """
 
99
    # Note:
 
100
    #  The DAV:owner and DAV:group live properties are only meaningful if you
 
101
    # are using ACL semantics (ie. Unix-like) which use them.  This (generic)
 
102
    # class does not.
 
103
 
 
104
    liveProperties = (
 
105
        (dav_namespace, "resourcetype"              ),
 
106
        (dav_namespace, "getetag"                   ),
 
107
        (dav_namespace, "getcontenttype"            ),
 
108
        (dav_namespace, "getcontentlength"          ),
 
109
        (dav_namespace, "getlastmodified"           ),
 
110
        (dav_namespace, "creationdate"              ),
 
111
        (dav_namespace, "displayname"               ),
 
112
        (dav_namespace, "supportedlock"             ),
 
113
       #(dav_namespace, "supported-report-set"      ), # RFC 3253, section 3.1.5
 
114
       #(dav_namespace, "owner"                     ), # RFC 3744, section 5.1
 
115
       #(dav_namespace, "group"                     ), # RFC 3744, section 5.2
 
116
       #(dav_namespace, "supported-privilege-set"   ), # RFC 3744, section 5.3
 
117
       #(dav_namespace, "current-user-privilege-set"), # RFC 3744, section 5.4
 
118
       #(dav_namespace, "acl"                       ), # RFC 3744, section 5.5
 
119
        (dav_namespace, "acl-restrictions"          ), # RFC 3744, section 5.6
 
120
       #(dav_namespace, "inherited-acl-set"         ), # RFC 3744, section 5.7
 
121
       #(dav_namespace, "principal-collection-set"  ), # RFC 3744, section 5.8
 
122
 
 
123
        (twisted_dav_namespace, "resource-class"),
 
124
    )
 
125
 
 
126
    def deadProperties(self):
 
127
        """
 
128
        Provides internal access to the WebDAV dead property store.  You
 
129
        probably shouldn't be calling this directly if you can use the property
 
130
        accessors in the L{IDAVResource} API instead.  However, a subclass must
 
131
        override this method to provide it's own dead property store.
 
132
 
 
133
        This implementation returns an instance of L{NonePropertyStore}, which
 
134
        cannot store dead properties.  Subclasses must override this method if
 
135
        they wish to store dead properties.
 
136
 
 
137
        @return: a dict-like object from which one can read and to which one can
 
138
            write dead properties.  Keys are qname tuples (ie. C{(namespace, name)})
 
139
            as returned by L{davxml.WebDAVElement.qname()} and values are
 
140
            L{davxml.WebDAVElement} instances.
 
141
        """
 
142
        if not hasattr(self, "_dead_properties"):
 
143
            self._dead_properties = NonePropertyStore(self)
 
144
        return self._dead_properties
 
145
 
 
146
    def hasProperty(self, property, request):
 
147
        """
 
148
        See L{IDAVResource.hasProperty}.
 
149
        """
 
150
        if type(property) is tuple:
 
151
            qname = property
 
152
        else:
 
153
            qname = property.qname()
 
154
 
 
155
        if qname[0] == twisted_private_namespace:
 
156
            return succeed(False)
 
157
 
 
158
        return succeed(qname in self.liveProperties or self.deadProperties().contains(qname))
 
159
 
 
160
    def readProperty(self, property, request):
 
161
        """
 
162
        See L{IDAVResource.readProperty}.
 
163
        """
 
164
        def defer():
 
165
            if type(property) is tuple:
 
166
                qname = property
 
167
                sname = "{%s}%s" % property
 
168
            else:
 
169
                qname = property.qname()
 
170
                sname = property.sname()
 
171
 
 
172
            namespace, name = qname
 
173
 
 
174
            if namespace == dav_namespace:
 
175
                if name == "resourcetype":
 
176
                    # Allow live property to be overriden by dead property
 
177
                    if self.deadProperties().contains(qname):
 
178
                        return self.deadProperties().get(qname)
 
179
                    if self.isCollection():
 
180
                        return davxml.ResourceType.collection
 
181
                    return davxml.ResourceType.empty
 
182
 
 
183
                if name == "getetag":
 
184
                    return davxml.GETETag(self.etag().generate())
 
185
 
 
186
                if name == "getcontenttype":
 
187
                    mimeType = self.contentType()
 
188
                    mimeType.params = None # WebDAV getcontenttype property does not include parameters
 
189
                    return davxml.GETContentType(generateContentType(mimeType))
 
190
 
 
191
                if name == "getcontentlength":
 
192
                    return davxml.GETContentLength(str(self.contentLength()))
 
193
 
 
194
                if name == "getlastmodified":
 
195
                    return davxml.GETLastModified.fromDate(self.lastModified())
 
196
 
 
197
                if name == "creationdate":
 
198
                    return davxml.CreationDate.fromDate(self.creationDate())
 
199
 
 
200
                if name == "displayname":
 
201
                    return davxml.DisplayName(self.displayName())
 
202
 
 
203
                if name == "supportedlock":
 
204
                    return davxml.SupportedLock(
 
205
                        davxml.LockEntry(davxml.LockScope.exclusive, davxml.LockType.write),
 
206
                        davxml.LockEntry(davxml.LockScope.shared   , davxml.LockType.write),
 
207
                    )
 
208
 
 
209
                if name == "acl-restrictions":
 
210
                    return davxml.ACLRestrictions()
 
211
 
 
212
            if namespace == twisted_dav_namespace:
 
213
                if name == "resource-class":
 
214
                    class ResourceClass (davxml.WebDAVTextElement):
 
215
                        namespace = twisted_dav_namespace
 
216
                        name = "resource-class"
 
217
                        hidden = False
 
218
                    return ResourceClass(self.__class__.__name__)
 
219
 
 
220
            if namespace == twisted_private_namespace:
 
221
                raise HTTPError(StatusResponse(
 
222
                    responsecode.FORBIDDEN,
 
223
                    "Properties in the %s namespace are private to the server." % (sname,)
 
224
                ))
 
225
 
 
226
            return self.deadProperties().get(qname)
 
227
 
 
228
        return maybeDeferred(defer)
 
229
 
 
230
    def writeProperty(self, property, request):
 
231
        """
 
232
        See L{IDAVResource.writeProperty}.
 
233
        """
 
234
        assert isinstance(property, davxml.WebDAVElement)
 
235
 
 
236
        def defer():
 
237
            if property.protected:
 
238
                raise HTTPError(StatusResponse(
 
239
                    responsecode.FORBIDDEN,
 
240
                    "Protected property %s may not be set." % (property.sname(),)
 
241
                ))
 
242
 
 
243
            if property.namespace == twisted_private_namespace:
 
244
                raise HTTPError(StatusResponse(
 
245
                    responsecode.FORBIDDEN,
 
246
                    "Properties in the %s namespace are private to the server." % (property.sname(),)
 
247
                ))
 
248
 
 
249
            return self.deadProperties().set(property)
 
250
 
 
251
        return maybeDeferred(defer)
 
252
 
 
253
    def removeProperty(self, property, request):
 
254
        """
 
255
        See L{IDAVResource.removeProperty}.
 
256
        """
 
257
        def defer():
 
258
            if type(property) is tuple:
 
259
                qname = property
 
260
                sname = "{%s}%s" % property
 
261
            else:
 
262
                qname = property.qname()
 
263
                sname = property.sname()
 
264
 
 
265
            if qname in self.liveProperties:
 
266
                raise HTTPError(StatusResponse(
 
267
                    responsecode.FORBIDDEN,
 
268
                    "Live property %s cannot be deleted." % (sname,)
 
269
                ))
 
270
 
 
271
            if qname[0] == twisted_private_namespace:
 
272
                raise HTTPError(StatusResponse(
 
273
                    responsecode.FORBIDDEN,
 
274
                    "Properties in the %s namespace are private to the server." % (sname,)
 
275
                ))
 
276
 
 
277
            return self.deadProperties().delete(qname)
 
278
 
 
279
        return maybeDeferred(defer)
 
280
 
 
281
    def listProperties(self, request):
 
282
        """
 
283
        See L{IDAVResource.listProperties}.
 
284
        """
 
285
        # FIXME: A set would be better here, that that's a python 2.4+ feature.
 
286
        qnames = list(self.liveProperties)
 
287
 
 
288
        for qname in self.deadProperties().list():
 
289
            if (qname not in qnames) and (qname[0] != twisted_private_namespace):
 
290
                qnames.append(qname)
 
291
 
 
292
        return succeed(qnames)
 
293
 
 
294
    def listAllprop(self, request):
 
295
        """
 
296
        Some DAV properties should not be returned to a C{DAV:allprop} query.
 
297
        RFC 3253 defines several such properties.  This method computes a subset
 
298
        of the property qnames returned by L{listProperties} by filtering out
 
299
        elements whose class have the C{.hidden} attribute set to C{True}.
 
300
        @return: a list of qnames of properties which are defined and are
 
301
            appropriate for use in response to a C{DAV:allprop} query.   
 
302
        """
 
303
        def doList(allnames):
 
304
            qnames = []
 
305
 
 
306
            for qname in allnames:
 
307
                try:
 
308
                    if not lookupElement(qname).hidden:
 
309
                        qnames.append(qname)
 
310
                except KeyError:
 
311
                    # Unknown element
 
312
                    qnames.append(qname)
 
313
 
 
314
            return qnames
 
315
 
 
316
        d = self.listProperties(request)
 
317
        d.addCallback(doList)
 
318
        return d
 
319
 
 
320
    def hasDeadProperty(self, property):
 
321
        """
 
322
        Same as L{hasProperty}, but bypasses the live property store and checks
 
323
        directly from the dead property store.
 
324
        """
 
325
        if type(property) is tuple:
 
326
            qname = property
 
327
        else:
 
328
            qname = property.qname()
 
329
 
 
330
        return self.deadProperties().contains(qname)
 
331
 
 
332
    def readDeadProperty(self, property):
 
333
        """
 
334
        Same as L{readProperty}, but bypasses the live property store and reads
 
335
        directly from the dead property store.
 
336
        """
 
337
        if type(property) is tuple:
 
338
            qname = property
 
339
        else:
 
340
            qname = property.qname()
 
341
 
 
342
        return self.deadProperties().get(qname)
 
343
 
 
344
    def writeDeadProperty(self, property):
 
345
        """
 
346
        Same as L{writeProperty}, but bypasses the live property store and
 
347
        writes directly to the dead property store.
 
348
        Note that this should not be used unless you know that you are writing
 
349
        to an overrideable live property, as this bypasses the logic which
 
350
        protects protected properties.  The result of writing to a
 
351
        non-overrideable live property with this method is undefined; the value
 
352
        in the dead property store may or may not be ignored when reading the
 
353
        property with L{readProperty}.
 
354
        """
 
355
        self.deadProperties().set(property)
 
356
 
 
357
    def removeDeadProperty(self, property):
 
358
        """
 
359
        Same as L{removeProperty}, but bypasses the live property store and acts
 
360
        directly on the dead property store.
 
361
        """
 
362
        if self.hasDeadProperty(property):
 
363
            if type(property) is tuple:
 
364
                qname = property
 
365
            else:
 
366
                qname = property.qname()
 
367
 
 
368
            self.deadProperties().delete(qname)
 
369
 
 
370
    #
 
371
    # Overrides some methods in MetaDataMixin in order to allow DAV properties
 
372
    # to override the values of some HTTP metadata.
 
373
    #
 
374
    def contentType(self):
 
375
        if self.hasDeadProperty((davxml.dav_namespace, "getcontenttype")):
 
376
            return self.readDeadProperty((davxml.dav_namespace, "getcontenttype")).mimeType()
 
377
        else:
 
378
            return super(DAVPropertyMixIn, self).contentType()
 
379
 
 
380
    def displayName(self):
 
381
        if self.hasDeadProperty((davxml.dav_namespace, "displayname")):
 
382
            return str(self.readDeadProperty((davxml.dav_namespace, "displayname")))
 
383
        else:
 
384
            return super(DAVPropertyMixIn, self).displayName()
 
385
 
 
386
class DAVResource (DAVPropertyMixIn, StaticRenderMixin):
 
387
    implements(IDAVResource)
 
388
 
 
389
    ##
 
390
    # DAV
 
391
    ##
 
392
 
 
393
    def davComplianceClasses(self):
 
394
        """
 
395
        This implementation raises L{NotImplementedError}.
 
396
        @return: a sequence of strings denoting WebDAV compliance classes.  For
 
397
            example, a DAV level 2 server might return ("1", "2").
 
398
        """
 
399
        unimplemented(self)
 
400
 
 
401
    def isCollection(self):
 
402
        """
 
403
        See L{IDAVResource.isCollection}.
 
404
 
 
405
        This implementation raises L{NotImplementedError}; a subclass must
 
406
        override this method.
 
407
        """
 
408
        unimplemented(self)
 
409
 
 
410
    def findChildren(self, depth):
 
411
        """
 
412
        See L{IDAVResource.findChildren}.
 
413
 
 
414
        This implementation raises returns C{()} if C{depth} is C{0} and this
 
415
        resource is a collection.  Otherwise, it raises L{NotImplementedError};
 
416
        a subclass must override this method.
 
417
        """
 
418
        assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,)
 
419
        if depth == "0" or not self.isCollection():
 
420
            return ()
 
421
        else:
 
422
            unimplemented(self)
 
423
 
 
424
    ##
 
425
    # ACL
 
426
    ##
 
427
 
 
428
    def principalCollections(self):
 
429
        """
 
430
        See L{IDAVResource.principalCollections}.
 
431
 
 
432
        This implementation returns C{()}.
 
433
        """
 
434
        return ()
 
435
 
 
436
    def accessControlList(self):
 
437
        """
 
438
        See L{IDAVResource.accessControlList}.
 
439
 
 
440
        This implementation returns an ACL granting all privileges to all
 
441
        principals.
 
442
        """
 
443
        return allACL
 
444
 
 
445
    def supportedPrivileges(self):
 
446
        """
 
447
        See L{IDAVResource.supportedPrivileges}.
 
448
 
 
449
        This implementation returns a supported privilege set containing only
 
450
        the DAV:all privilege.
 
451
        """
 
452
        return allPrivilegeSet
 
453
 
 
454
    ##
 
455
    # HTTP
 
456
    ##
 
457
 
 
458
    def renderHTTP(self, request):
 
459
 
 
460
        # FIXME: This is for testing with litmus; comment out when not in use
 
461
        #litmus = request.headers.getRawHeaders("x-litmus")
 
462
        #if litmus: log.msg("*** Litmus test: %s ***" % (litmus,))
 
463
 
 
464
        # FIXME: Learn how to use twisted logging facility, wsanchez
 
465
        protocol = "HTTP/%s.%s" % request.clientproto
 
466
        log.msg("%s %s %s" % (request.method, urllib.unquote(request.uri), protocol))
 
467
 
 
468
        #
 
469
        # If this is a collection and the URI doesn't end in "/", redirect.
 
470
        #
 
471
        if self.isCollection() and request.uri[-1:] != "/":
 
472
            return RedirectResponse(request.uri + "/")
 
473
 
 
474
        def setHeaders(response):
 
475
            response = IResponse(response)
 
476
 
 
477
            response.headers.setHeader("dav", self.davComplianceClasses())
 
478
 
 
479
            #
 
480
            # If this is a collection and the URI doesn't end in "/", add a
 
481
            # Content-Location header.  This is needed even if we redirect such
 
482
            # requests (as above) in the event that this resource was created or
 
483
            # modified by the request.
 
484
            #
 
485
            if self.isCollection() and request.uri[-1:] != "/":
 
486
                response.headers.setHeader("content-location", request.uri + "/")
 
487
 
 
488
            return response
 
489
 
 
490
        def onError(f):
 
491
            # If we get an HTTPError, run its response through setHeaders() as
 
492
            # well.
 
493
            f.trap(HTTPError)
 
494
            return setHeaders(f.value.response)
 
495
 
 
496
        d = maybeDeferred(super(DAVResource, self).renderHTTP, request)
 
497
        return d.addCallbacks(setHeaders, onError)
 
498
 
 
499
class DAVLeafResource (DAVResource, LeafResource):
 
500
    """
 
501
    DAV resource with no children.
 
502
    """
 
503
    def findChildren(self, depth):
 
504
        return ()
 
505
 
 
506
##
 
507
# Utilities
 
508
##
 
509
 
 
510
allACL = davxml.ACL(
 
511
    davxml.ACE(
 
512
        davxml.Principal(davxml.All()),
 
513
        davxml.Grant(davxml.Privilege(davxml.All())),
 
514
        davxml.Protected()
 
515
    )
 
516
)
 
517
 
 
518
allPrivilegeSet = davxml.SupportedPrivilegeSet(
 
519
    davxml.SupportedPrivilege(
 
520
        davxml.Privilege(davxml.All()),
 
521
        davxml.Description("all privileges", **{"xml:lang": "en"})
 
522
    )
 
523
)