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
72
def qname(self): return (self.namespace, self.name)
73
def sname(self): return "{%s}%s" % (self.namespace, self.name)
75
qname = classmethod(qname)
76
sname = classmethod(sname)
78
def __init__(self, *children, **attributes):
79
super(WebDAVElement, self).__init__()
81
if self.allowed_children is None:
82
raise NotImplementedError("WebDAVElement subclass %s is not implemented."
83
% (self.__class__.__name__,))
86
# Validate that children are of acceptable types
88
allowed_children = dict([
89
(child_type, list(limits))
90
for child_type, limits
91
in self.allowed_children.items()
96
for child in children:
97
if child is None: continue
99
assert isinstance(child, (WebDAVElement, PCDATAElement)), "Not an element: %r" % (child,)
101
for allowed, (min, max) in allowed_children.items():
102
if type(allowed) == type and isinstance(child, allowed):
103
qname = allowed.qname()
104
elif child.qname() == allowed:
109
if min is not None and min > 0: min -= 1
111
assert max > 0, "Too many children of type %s for %s" % (child.sname(), self.sname())
113
allowed_children[qname] = (min, max)
114
my_children.append(child)
117
if not (isinstance(child, PCDATAElement) and child.isWhitespace()):
118
log.msg("Child of type %s is unexpected and therefore ignored in %s element"
119
% (child.sname(), self.sname()))
121
for qname, (min, max) in allowed_children.items():
123
raise ValueError("Not enough children of type {%s}%s for %s"
124
% (qname[0], qname[1], self.sname()))
126
self.children = my_children
129
# Validate that attributes have known names
133
if self.allowed_attributes:
134
for name in attributes:
135
if name in self.allowed_attributes:
136
my_attributes[name] = attributes[name]
138
log.msg("Attribute %s is unexpected and therefore ignored in %s element"
139
% (name, self.sname()))
141
for name, required in self.allowed_attributes.items():
142
if required and name not in my_attributes:
143
raise ValueError("Attribute %s is required in %s element"
144
% (name, self.sname()))
146
elif not isinstance(self, WebDAVUnknownElement):
148
log.msg("Attributes %s are unexpected and therefore ignored in %s element"
149
% (attributes.keys(), self.sname()))
151
self.attributes = my_attributes
154
if hasattr(self, "attributes") and hasattr(self, "children"):
155
return "<%s %r: %r>" % (self.sname(), self.attributes, self.children)
157
return "<%s>" % (self.sname())
159
def __eq__(self, other):
160
if isinstance(other, WebDAVElement):
162
self.name == other.name and
163
self.namespace == other.namespace and
164
self.attributes == other.attributes and
165
self.children == other.children
168
return NotImplemented
170
def __ne__(self, other):
171
return not self.__eq__(other)
173
def __contains__(self, child):
174
return child in self.children
176
def writeXML(self, output):
177
document = xml.dom.minidom.Document()
178
self.addToDOM(document, None)
179
PrintXML(document, stream=output)
182
output = StringIO.StringIO()
183
self.writeXML(output)
184
return output.getvalue()
186
def element(self, document):
187
element = document.createElementNS(self.namespace, self.name)
188
if hasattr(self, "attributes"):
189
for name, value in self.attributes.items():
190
namespace, name = decodeXMLName(name)
191
attribute = document.createAttributeNS(namespace, name)
192
attribute.nodeValue = value
193
element.setAttributeNodeNS(attribute)
196
def addToDOM(self, document, parent):
197
element = self.element(document)
200
document.appendChild(element)
202
parent.appendChild(element)
204
for child in self.children:
207
child.addToDOM(document, element)
209
log.err("Unable to add child %r of element %s to DOM" % (child, self))
212
def childrenOfType(self, child_type):
214
Returns a list of children of the given type.
216
return [ c for c in self.children if isinstance(c, child_type) ]
218
def childOfType(self, child_type):
220
Returns a child of the given type, if any, or None.
221
Raises ValueError if more than one is found.
224
for child in self.childrenOfType(child_type):
226
raise ValueError("Multiple %s elements found in %s" % (child_type.sname(), self))
230
class PCDATAElement (object):
231
def sname(self): return "#PCDATA"
233
qname = classmethod(sname)
234
sname = classmethod(sname)
236
def __init__(self, data):
237
super(PCDATAElement, self).__init__()
241
elif type(data) is unicode:
242
data = data.encode("utf-8")
244
assert type(data) is str, ("PCDATA must be a string: %r" % (data,))
249
return str(self.data)
252
return "<%s: %r>" % (self.__class__.__name__, self.data)
254
def __add__(self, other):
255
if isinstance(other, PCDATAElement):
256
return self.__class__(self.data + other.data)
258
return self.__class__(self.data + other)
260
def __eq__(self, other):
261
if isinstance(other, PCDATAElement):
262
return self.data == other.data
263
elif type(other) in (str, unicode):
264
return self.data == other
266
return NotImplemented
268
def __ne__(self, other):
269
return not self.__eq__(other)
271
def isWhitespace(self):
272
for char in str(self):
273
if char not in string.whitespace:
277
def element(self, document):
278
return document.createTextNode(self.data)
280
def addToDOM(self, document, parent):
282
parent.appendChild(self.element(document))
284
log.err("Invalid PCDATA: %r" % (self.data,))
287
class WebDAVOneShotElement (WebDAVElement):
289
Element with exactly one WebDAVEmptyElement child.
293
def __new__(clazz, *children):
295
for next in children:
296
if isinstance(next, WebDAVEmptyElement):
297
if child is not None:
298
raise ValueError("%s must have exactly one child, not %r"
299
% (clazz.__name__, children))
301
elif isinstance(next, PCDATAElement):
304
raise ValueError("%s child is not a WebDAVEmptyElement instance: %s"
305
% (clazz.__name__, next))
307
if clazz not in WebDAVOneShotElement.__singletons:
308
WebDAVOneShotElement.__singletons[clazz] = {
309
child: WebDAVElement.__new__(clazz, children)
311
elif child not in WebDAVOneShotElement.__singletons[clazz]:
312
WebDAVOneShotElement.__singletons[clazz][child] = (
313
WebDAVElement.__new__(clazz, children)
316
return WebDAVOneShotElement.__singletons[clazz][child]
318
class WebDAVUnknownElement (WebDAVElement):
320
Placeholder for unknown element tag names.
323
WebDAVElement: (0, None),
324
PCDATAElement: (0, None),
327
class WebDAVEmptyElement (WebDAVElement):
329
WebDAV element with no contents.
333
def __new__(clazz, *args, **kwargs):
337
return WebDAVElement.__new__(clazz, **kwargs)
339
if clazz not in WebDAVEmptyElement.__singletons:
340
WebDAVEmptyElement.__singletons[clazz] = (WebDAVElement.__new__(clazz))
341
return WebDAVEmptyElement.__singletons[clazz]
343
allowed_children = {}
347
class WebDAVTextElement (WebDAVElement):
349
WebDAV element containing PCDATA.
351
def fromString(clazz, string):
354
elif type(string) is str:
355
return clazz(PCDATAElement(string))
356
elif type(string) is unicode:
357
return clazz(PCDATAElement(string.encode("utf-8")))
359
return clazz(PCDATAElement(str(string)))
361
fromString = classmethod(fromString)
363
allowed_children = { PCDATAElement: (0, None) }
366
return "".join([c.data for c in self.children])
371
return "<%s: %r>" % (self.sname(), content)
373
return "<%s>" % (self.sname(),)
375
def __eq__(self, other):
376
if isinstance(other, WebDAVTextElement):
377
return str(self) == str(other)
378
elif type(other) in (str, unicode):
379
return str(self) == other
381
return NotImplemented
383
class WebDAVDateTimeElement (WebDAVTextElement):
385
WebDAV date-time element. (RFC 2518, section 23.2)
387
def fromDate(clazz, date):
389
date may be a datetime.datetime instance, a POSIX timestamp
390
(integer value, such as returned by time.time()), or an ISO
391
8601-formatted (eg. "2005-06-13T16:14:11Z") date/time string.
394
if date.utcoffset() is None:
395
return date.isoformat() + "Z"
397
return date.isoformat()
399
if type(date) is int:
400
date = isoformat(datetime.datetime.fromtimestamp(date))
401
elif type(date) is str:
403
elif type(date) is unicode:
404
date = date.encode("utf-8")
405
elif isinstance(date, datetime.datetime):
406
date = isoformat(date)
408
raise ValueError("Unknown date type: %r" % (date,))
410
return clazz(PCDATAElement(date))
412
fromDate = classmethod(fromDate)
414
def __init__(self, *children, **attributes):
415
super(WebDAVDateTimeElement, self).__init__(*children, **attributes)
416
self.datetime() # Raise ValueError if the format is wrong
418
def __eq__(self, other):
419
if isinstance(other, WebDAVDateTimeElement):
420
return self.datetime() == other.datetime()
422
return NotImplemented
431
class DateTimeHeaderElement (WebDAVTextElement):
433
WebDAV date-time element for elements that substitute for HTTP
434
headers. (RFC 2068, section 3.3.1)
436
def fromDate(clazz, date):
438
date may be a datetime.datetime instance, a POSIX timestamp
439
(integer value, such as returned by time.time()), or an RFC
440
2068 Full Date (eg. "Mon, 23 May 2005 04:52:22 GMT") string.
444
# FIXME: strftime() is subject to localization nonsense; we need to
445
# ensure that we're using the correct localization, or don't use
448
return date.strftime("%a, %d %b %Y %H:%M:%S GMT")
450
if type(date) is int:
451
date = format(datetime.datetime.fromtimestamp(date))
452
elif type(date) is str:
454
elif type(date) is unicode:
455
date = date.encode("utf-8")
456
elif isinstance(date, datetime.datetime):
458
raise NotImplementedError("I need to normalize to UTC")
461
raise ValueError("Unknown date type: %r" % (date,))
463
return clazz(PCDATAElement(date))
465
fromDate = classmethod(fromDate)
467
def __init__(self, *children, **attributes):
468
super(DateTimeHeaderElement, self).__init__(*children, **attributes)
469
self.datetime() # Raise ValueError if the format is wrong
471
def __eq__(self, other):
472
if isinstance(other, WebDAVDateTimeElement):
473
return self.datetime() == other.datetime()
475
return NotImplemented
482
return parseDateTime(s)
488
class FixedOffset (datetime.tzinfo):
490
Fixed offset in minutes east from UTC.
492
def __init__(self, offset, name=None):
493
super(FixedOffset, self).__init__()
495
self._offset = datetime.timedelta(minutes=offset)
498
def utcoffset(self, dt): return self._offset
499
def tzname (self, dt): return self._name
500
def dst (self, dt): return datetime.timedelta(0)
502
def parse_date(date):
504
Parse an ISO 8601 date and return a corresponding datetime.datetime object.
506
# See http://www.iso.org/iso/en/prods-services/popstds/datesandtime.html
510
if regex_date is None:
513
regex_date = re.compile(
515
"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})T" +
516
"(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2})(?:.(?P<subsecond>\d+))*" +
517
"(?:Z|(?P<offset_sign>\+|-)(?P<offset_hour>\d{2}):(?P<offset_minute>\d{2}))" +
521
match = regex_date.match(date)
522
if match is not None:
523
subsecond = match.group("subsecond")
524
if subsecond is None:
527
subsecond = int(subsecond)
529
offset_sign = match.group("offset_sign")
530
if offset_sign is None:
531
offset = FixedOffset(0)
533
offset_hour = int(match.group("offset_hour" ))
534
offset_minute = int(match.group("offset_minute"))
536
delta = (offset_hour * 60) + offset_minute
538
if offset_sign == "+": offset = FixedOffset(0 - delta)
539
elif offset_sign == "-": offset = FixedOffset(0 + delta)
541
return datetime.datetime(
542
int(match.group("year" )),
543
int(match.group("month" )),
544
int(match.group("day" )),
545
int(match.group("hour" )),
546
int(match.group("minute")),
547
int(match.group("second")),
552
raise ValueError("Invalid ISO 8601 date format: %r" % (date,))