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
26
WebDAV XML base classes.
28
This module provides XML utilities for use with WebDAV.
30
See RFC 2518: http://www.ietf.org/rfc/rfc2518.txt (WebDAV)
37
"WebDAVOneShotElement",
38
"WebDAVUnknownElement",
41
"WebDAVDateTimeElement",
42
"DateTimeHeaderElement",
47
import xml.dom.minidom
51
from twisted.python import log
52
from twisted.web2.http_headers import parseDateTime
53
from twisted.web2.dav.element.util import PrintXML, decodeXMLName
59
dav_namespace = "DAV:"
61
class WebDAVElement (object):
63
WebDAV XML element. (RFC 2518, section 12)
65
namespace = dav_namespace # Element namespace (class variable)
66
name = None # Element name (class variable)
67
allowed_children = None # Types & count limits on child elements
68
allowed_attributes = None # Allowed attribute names
69
hidden = False # Don't list in PROPFIND with <allprop>
70
protected = False # See RFC 3253 section 1.4.1
71
unregistered = False # Subclass of factory; doesn't register
74
return (self.namespace, self.name)
77
return "{%s}%s" % (self.namespace, self.name)
79
qname = classmethod(qname)
80
sname = classmethod(sname)
82
def __init__(self, *children, **attributes):
83
super(WebDAVElement, self).__init__()
85
if self.allowed_children is None:
86
raise NotImplementedError("WebDAVElement subclass %s is not implemented."
87
% (self.__class__.__name__,))
90
# Validate that children are of acceptable types
92
allowed_children = dict([
93
(child_type, list(limits))
94
for child_type, limits
95
in self.allowed_children.items()
100
for child in children:
104
if isinstance(child, (str, unicode)):
105
child = PCDATAElement(child)
107
assert isinstance(child, (WebDAVElement, PCDATAElement)), "Not an element: %r" % (child,)
109
for allowed, (min, max) in allowed_children.items():
110
if type(allowed) == type and isinstance(child, allowed):
112
elif child.qname() == allowed:
117
if min is not None and min > 0:
120
assert max > 0, "Too many children of type %s for %s" % (child.sname(), self.sname())
122
allowed_children[qname] = (min, max)
123
my_children.append(child)
126
if not (isinstance(child, PCDATAElement) and child.isWhitespace()):
127
log.msg("Child of type %s is unexpected and therefore ignored in %s element"
128
% (child.sname(), self.sname()))
130
for qname, (min, max) in allowed_children.items():
132
raise ValueError("Not enough children of type {%s}%s for %s"
133
% (qname[0], qname[1], self.sname()))
135
self.children = tuple(my_children)
138
# Validate that attributes have known names
142
if self.allowed_attributes:
143
for name in attributes:
144
if name in self.allowed_attributes:
145
my_attributes[name] = attributes[name]
147
log.msg("Attribute %s is unexpected and therefore ignored in %s element"
148
% (name, self.sname()))
150
for name, required in self.allowed_attributes.items():
151
if required and name not in my_attributes:
152
raise ValueError("Attribute %s is required in %s element"
153
% (name, self.sname()))
155
elif not isinstance(self, WebDAVUnknownElement):
157
log.msg("Attributes %s are unexpected and therefore ignored in %s element"
158
% (attributes.keys(), self.sname()))
160
self.attributes = my_attributes
166
if hasattr(self, "attributes") and hasattr(self, "children"):
167
return "<%s %r: %r>" % (self.sname(), self.attributes, self.children)
169
return "<%s>" % (self.sname())
171
def __eq__(self, other):
172
if isinstance(other, WebDAVElement):
174
self.name == other.name and
175
self.namespace == other.namespace and
176
self.attributes == other.attributes and
177
self.children == other.children
180
return NotImplemented
182
def __ne__(self, other):
183
return not self.__eq__(other)
185
def __contains__(self, child):
186
return child in self.children
188
def writeXML(self, output):
189
document = xml.dom.minidom.Document()
190
self.addToDOM(document, None)
191
PrintXML(document, stream=output)
194
output = StringIO.StringIO()
195
self.writeXML(output)
196
return output.getvalue()
198
def element(self, document):
199
element = document.createElementNS(self.namespace, self.name)
200
if hasattr(self, "attributes"):
201
for name, value in self.attributes.items():
202
namespace, name = decodeXMLName(name)
203
attribute = document.createAttributeNS(namespace, name)
204
attribute.nodeValue = value
205
element.setAttributeNodeNS(attribute)
208
def addToDOM(self, document, parent):
209
element = self.element(document)
212
document.appendChild(element)
214
parent.appendChild(element)
216
for child in self.children:
219
child.addToDOM(document, element)
221
log.err("Unable to add child %r of element %s to DOM" % (child, self))
224
def childrenOfType(self, child_type):
226
Returns a list of children with the same qname as the given type.
228
if type(child_type) is tuple:
231
qname = child_type.qname()
233
return [ c for c in self.children if c.qname() == qname ]
235
def childOfType(self, child_type):
237
Returns a child of the given type, if any, or None.
238
Raises ValueError if more than one is found.
241
for child in self.childrenOfType(child_type):
243
raise ValueError("Multiple %s elements found in %s" % (child_type.sname(), self.toxml()))
247
class PCDATAElement (object):
248
def sname(self): return "#PCDATA"
250
qname = classmethod(sname)
251
sname = classmethod(sname)
253
def __init__(self, data):
254
super(PCDATAElement, self).__init__()
258
elif type(data) is unicode:
259
data = data.encode("utf-8")
261
assert type(data) is str, ("PCDATA must be a string: %r" % (data,))
266
return str(self.data)
269
return "<%s: %r>" % (self.__class__.__name__, self.data)
271
def __add__(self, other):
272
if isinstance(other, PCDATAElement):
273
return self.__class__(self.data + other.data)
275
return self.__class__(self.data + other)
277
def __eq__(self, other):
278
if isinstance(other, PCDATAElement):
279
return self.data == other.data
280
elif type(other) in (str, unicode):
281
return self.data == other
283
return NotImplemented
285
def __ne__(self, other):
286
return not self.__eq__(other)
288
def isWhitespace(self):
289
for char in str(self):
290
if char not in string.whitespace:
294
def element(self, document):
295
return document.createTextNode(self.data)
297
def addToDOM(self, document, parent):
299
parent.appendChild(self.element(document))
301
log.err("Invalid PCDATA: %r" % (self.data,))
304
class WebDAVOneShotElement (WebDAVElement):
306
Element with exactly one WebDAVEmptyElement child and no attributes.
310
def __new__(clazz, *children):
312
for next in children:
313
if isinstance(next, WebDAVEmptyElement):
314
if child is not None:
315
raise ValueError("%s must have exactly one child, not %r"
316
% (clazz.__name__, children))
318
elif isinstance(next, PCDATAElement):
321
raise ValueError("%s child is not a WebDAVEmptyElement instance: %s"
322
% (clazz.__name__, next))
324
if clazz not in WebDAVOneShotElement.__singletons:
325
WebDAVOneShotElement.__singletons[clazz] = {
326
child: WebDAVElement.__new__(clazz, children)
328
elif child not in WebDAVOneShotElement.__singletons[clazz]:
329
WebDAVOneShotElement.__singletons[clazz][child] = (
330
WebDAVElement.__new__(clazz, children)
333
return WebDAVOneShotElement.__singletons[clazz][child]
335
class WebDAVUnknownElement (WebDAVElement):
337
Placeholder for unknown element tag names.
340
WebDAVElement: (0, None),
341
PCDATAElement: (0, None),
344
class WebDAVEmptyElement (WebDAVElement):
346
WebDAV element with no contents.
350
def __new__(clazz, *args, **kwargs):
354
return WebDAVElement.__new__(clazz, **kwargs)
356
if clazz not in WebDAVEmptyElement.__singletons:
357
WebDAVEmptyElement.__singletons[clazz] = (WebDAVElement.__new__(clazz))
358
return WebDAVEmptyElement.__singletons[clazz]
360
allowed_children = {}
364
class WebDAVTextElement (WebDAVElement):
366
WebDAV element containing PCDATA.
368
def fromString(clazz, string):
371
elif isinstance(string, (str, unicode)):
372
return clazz(PCDATAElement(string))
374
return clazz(PCDATAElement(str(string)))
376
fromString = classmethod(fromString)
378
allowed_children = { PCDATAElement: (0, None) }
381
return "".join([c.data for c in self.children])
386
return "<%s: %r>" % (self.sname(), content)
388
return "<%s>" % (self.sname(),)
390
def __eq__(self, other):
391
if isinstance(other, WebDAVTextElement):
392
return str(self) == str(other)
393
elif type(other) in (str, unicode):
394
return str(self) == other
396
return NotImplemented
398
class WebDAVDateTimeElement (WebDAVTextElement):
400
WebDAV date-time element. (RFC 2518, section 23.2)
402
def fromDate(clazz, date):
404
date may be a datetime.datetime instance, a POSIX timestamp
405
(integer value, such as returned by time.time()), or an ISO
406
8601-formatted (eg. "2005-06-13T16:14:11Z") date/time string.
409
if date.utcoffset() is None:
410
return date.isoformat() + "Z"
412
return date.isoformat()
414
if type(date) is int:
415
date = isoformat(datetime.datetime.fromtimestamp(date))
416
elif type(date) is str:
418
elif type(date) is unicode:
419
date = date.encode("utf-8")
420
elif isinstance(date, datetime.datetime):
421
date = isoformat(date)
423
raise ValueError("Unknown date type: %r" % (date,))
425
return clazz(PCDATAElement(date))
427
fromDate = classmethod(fromDate)
429
def __init__(self, *children, **attributes):
430
super(WebDAVDateTimeElement, self).__init__(*children, **attributes)
431
self.datetime() # Raise ValueError if the format is wrong
433
def __eq__(self, other):
434
if isinstance(other, WebDAVDateTimeElement):
435
return self.datetime() == other.datetime()
437
return NotImplemented
446
class DateTimeHeaderElement (WebDAVTextElement):
448
WebDAV date-time element for elements that substitute for HTTP
449
headers. (RFC 2068, section 3.3.1)
451
def fromDate(clazz, date):
453
date may be a datetime.datetime instance, a POSIX timestamp
454
(integer value, such as returned by time.time()), or an RFC
455
2068 Full Date (eg. "Mon, 23 May 2005 04:52:22 GMT") string.
459
# FIXME: strftime() is subject to localization nonsense; we need to
460
# ensure that we're using the correct localization, or don't use
463
return date.strftime("%a, %d %b %Y %H:%M:%S GMT")
465
if type(date) is int:
466
date = format(datetime.datetime.fromtimestamp(date))
467
elif type(date) is str:
469
elif type(date) is unicode:
470
date = date.encode("utf-8")
471
elif isinstance(date, datetime.datetime):
473
raise NotImplementedError("I need to normalize to UTC")
476
raise ValueError("Unknown date type: %r" % (date,))
478
return clazz(PCDATAElement(date))
480
fromDate = classmethod(fromDate)
482
def __init__(self, *children, **attributes):
483
super(DateTimeHeaderElement, self).__init__(*children, **attributes)
484
self.datetime() # Raise ValueError if the format is wrong
486
def __eq__(self, other):
487
if isinstance(other, WebDAVDateTimeElement):
488
return self.datetime() == other.datetime()
490
return NotImplemented
497
return parseDateTime(s)
503
class FixedOffset (datetime.tzinfo):
505
Fixed offset in minutes east from UTC.
507
def __init__(self, offset, name=None):
508
super(FixedOffset, self).__init__()
510
self._offset = datetime.timedelta(minutes=offset)
513
def utcoffset(self, dt): return self._offset
514
def tzname (self, dt): return self._name
515
def dst (self, dt): return datetime.timedelta(0)
517
def parse_date(date):
519
Parse an ISO 8601 date and return a corresponding datetime.datetime object.
521
# See http://www.iso.org/iso/en/prods-services/popstds/datesandtime.html
525
if regex_date is None:
528
regex_date = re.compile(
530
"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T" +
531
"(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(?:.(?P<subsecond>\d+))*" +
532
"(?:Z|(?P<offset_sign>\+|-)(?P<offset_hour>\d{2}):(?P<offset_minute>\d{2}))" +
536
match = regex_date.match(date)
537
if match is not None:
538
subsecond = match.group("subsecond")
539
if subsecond is None:
542
subsecond = int(subsecond)
544
offset_sign = match.group("offset_sign")
545
if offset_sign is None:
546
offset = FixedOffset(0)
548
offset_hour = int(match.group("offset_hour" ))
549
offset_minute = int(match.group("offset_minute"))
551
delta = (offset_hour * 60) + offset_minute
553
if offset_sign == "+": offset = FixedOffset(0 - delta)
554
elif offset_sign == "-": offset = FixedOffset(0 + delta)
556
return datetime.datetime(
557
int(match.group("year" )),
558
int(match.group("month" )),
559
int(match.group("day" )),
560
int(match.group("hour" )),
561
int(match.group("minute")),
562
int(match.group("second")),
567
raise ValueError("Invalid ISO 8601 date format: %r" % (date,))