2
# Copyright (c) 2005 Apple Computer, Inc. All rights reserved.
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:
11
# The above copyright notice and this permission notice shall be included in all
12
# copies or substantial portions of the Software.
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
22
# DRI: Wilfredo Sanchez, wsanchez@apple.com
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
52
twisted_dav_namespace = "http://twistedmatrix.com/xml_namespace/dav/"
53
twisted_private_namespace = "http://twistedmatrix.com/xml_namespace/dav/private/"
55
class DAVPropertyMixIn (MetaDataMixin):
57
Mix-in class which implements the DAV property access API in
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:
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.
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.
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.
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
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.
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.
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)
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
123
(twisted_dav_namespace, "resource-class"),
126
def deadProperties(self):
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.
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.
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.
142
if not hasattr(self, "_dead_properties"):
143
self._dead_properties = NonePropertyStore(self)
144
return self._dead_properties
146
def hasProperty(self, property, request):
148
See L{IDAVResource.hasProperty}.
150
if type(property) is tuple:
153
qname = property.qname()
155
if qname[0] == twisted_private_namespace:
156
return succeed(False)
158
return succeed(qname in self.liveProperties or self.deadProperties().contains(qname))
160
def readProperty(self, property, request):
162
See L{IDAVResource.readProperty}.
165
if type(property) is tuple:
167
sname = "{%s}%s" % property
169
qname = property.qname()
170
sname = property.sname()
172
namespace, name = qname
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
183
if name == "getetag":
184
return davxml.GETETag(self.etag().generate())
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))
191
if name == "getcontentlength":
192
return davxml.GETContentLength(str(self.contentLength()))
194
if name == "getlastmodified":
195
return davxml.GETLastModified.fromDate(self.lastModified())
197
if name == "creationdate":
198
return davxml.CreationDate.fromDate(self.creationDate())
200
if name == "displayname":
201
return davxml.DisplayName(self.displayName())
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),
209
if name == "acl-restrictions":
210
return davxml.ACLRestrictions()
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"
218
return ResourceClass(self.__class__.__name__)
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,)
226
return self.deadProperties().get(qname)
228
return maybeDeferred(defer)
230
def writeProperty(self, property, request):
232
See L{IDAVResource.writeProperty}.
234
assert isinstance(property, davxml.WebDAVElement)
237
if property.protected:
238
raise HTTPError(StatusResponse(
239
responsecode.FORBIDDEN,
240
"Protected property %s may not be set." % (property.sname(),)
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(),)
249
return self.deadProperties().set(property)
251
return maybeDeferred(defer)
253
def removeProperty(self, property, request):
255
See L{IDAVResource.removeProperty}.
258
if type(property) is tuple:
260
sname = "{%s}%s" % property
262
qname = property.qname()
263
sname = property.sname()
265
if qname in self.liveProperties:
266
raise HTTPError(StatusResponse(
267
responsecode.FORBIDDEN,
268
"Live property %s cannot be deleted." % (sname,)
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,)
277
return self.deadProperties().delete(qname)
279
return maybeDeferred(defer)
281
def listProperties(self, request):
283
See L{IDAVResource.listProperties}.
285
# FIXME: A set would be better here, that that's a python 2.4+ feature.
286
qnames = list(self.liveProperties)
288
for qname in self.deadProperties().list():
289
if (qname not in qnames) and (qname[0] != twisted_private_namespace):
292
return succeed(qnames)
294
def listAllprop(self, request):
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.
303
def doList(allnames):
306
for qname in allnames:
308
if not lookupElement(qname).hidden:
316
d = self.listProperties(request)
317
d.addCallback(doList)
320
def hasDeadProperty(self, property):
322
Same as L{hasProperty}, but bypasses the live property store and checks
323
directly from the dead property store.
325
if type(property) is tuple:
328
qname = property.qname()
330
return self.deadProperties().contains(qname)
332
def readDeadProperty(self, property):
334
Same as L{readProperty}, but bypasses the live property store and reads
335
directly from the dead property store.
337
if type(property) is tuple:
340
qname = property.qname()
342
return self.deadProperties().get(qname)
344
def writeDeadProperty(self, property):
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}.
355
self.deadProperties().set(property)
357
def removeDeadProperty(self, property):
359
Same as L{removeProperty}, but bypasses the live property store and acts
360
directly on the dead property store.
362
if self.hasDeadProperty(property):
363
if type(property) is tuple:
366
qname = property.qname()
368
self.deadProperties().delete(qname)
371
# Overrides some methods in MetaDataMixin in order to allow DAV properties
372
# to override the values of some HTTP metadata.
374
def contentType(self):
375
if self.hasDeadProperty((davxml.dav_namespace, "getcontenttype")):
376
return self.readDeadProperty((davxml.dav_namespace, "getcontenttype")).mimeType()
378
return super(DAVPropertyMixIn, self).contentType()
380
def displayName(self):
381
if self.hasDeadProperty((davxml.dav_namespace, "displayname")):
382
return str(self.readDeadProperty((davxml.dav_namespace, "displayname")))
384
return super(DAVPropertyMixIn, self).displayName()
386
class DAVResource (DAVPropertyMixIn, StaticRenderMixin):
387
implements(IDAVResource)
393
def davComplianceClasses(self):
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").
401
def isCollection(self):
403
See L{IDAVResource.isCollection}.
405
This implementation raises L{NotImplementedError}; a subclass must
406
override this method.
410
def findChildren(self, depth):
412
See L{IDAVResource.findChildren}.
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.
418
assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,)
419
if depth == "0" or not self.isCollection():
428
def principalCollections(self):
430
See L{IDAVResource.principalCollections}.
432
This implementation returns C{()}.
436
def accessControlList(self):
438
See L{IDAVResource.accessControlList}.
440
This implementation returns an ACL granting all privileges to all
445
def supportedPrivileges(self):
447
See L{IDAVResource.supportedPrivileges}.
449
This implementation returns a supported privilege set containing only
450
the DAV:all privilege.
452
return allPrivilegeSet
458
def renderHTTP(self, request):
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,))
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))
469
# If this is a collection and the URI doesn't end in "/", redirect.
471
if self.isCollection() and request.uri[-1:] != "/":
472
return RedirectResponse(request.uri + "/")
474
def setHeaders(response):
475
response = IResponse(response)
477
response.headers.setHeader("dav", self.davComplianceClasses())
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.
485
if self.isCollection() and request.uri[-1:] != "/":
486
response.headers.setHeader("content-location", request.uri + "/")
491
# If we get an HTTPError, run its response through setHeaders() as
494
return setHeaders(f.value.response)
496
d = maybeDeferred(super(DAVResource, self).renderHTTP, request)
497
return d.addCallbacks(setHeaders, onError)
499
class DAVLeafResource (DAVResource, LeafResource):
501
DAV resource with no children.
503
def findChildren(self, depth):
512
davxml.Principal(davxml.All()),
513
davxml.Grant(davxml.Privilege(davxml.All())),
518
allPrivilegeSet = davxml.SupportedPrivilegeSet(
519
davxml.SupportedPrivilege(
520
davxml.Privilege(davxml.All()),
521
davxml.Description("all privileges", **{"xml:lang": "en"})