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

« back to all changes in this revision

Viewing changes to twisted/web2/dav/element/base.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 XML base classes.
 
27
 
 
28
This module provides XML utilities for use with WebDAV.
 
29
 
 
30
See RFC 2518: http://www.ietf.org/rfc/rfc2518.txt (WebDAV)
 
31
"""
 
32
 
 
33
__all__ = [
 
34
    "dav_namespace",
 
35
    "WebDAVElement",
 
36
    "PCDATAElement",
 
37
    "WebDAVOneShotElement",
 
38
    "WebDAVUnknownElement",
 
39
    "WebDAVEmptyElement",
 
40
    "WebDAVTextElement",
 
41
    "WebDAVDateTimeElement",
 
42
    "DateTimeHeaderElement",    
 
43
]
 
44
 
 
45
import string
 
46
import StringIO
 
47
import xml.dom.minidom
 
48
 
 
49
import datetime
 
50
 
 
51
from twisted.python import log
 
52
from twisted.web2.http_headers import parseDateTime
 
53
from twisted.web2.dav.element.util import PrintXML, decodeXMLName
 
54
 
 
55
##
 
56
# Base XML elements
 
57
##
 
58
 
 
59
dav_namespace = "DAV:"
 
60
 
 
61
class WebDAVElement (object):
 
62
    """
 
63
    WebDAV XML element. (RFC 2518, section 12)
 
64
    """
 
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
 
72
 
 
73
    def qname(self):
 
74
        return (self.namespace, self.name)
 
75
 
 
76
    def sname(self):
 
77
        return "{%s}%s" % (self.namespace, self.name)
 
78
 
 
79
    qname = classmethod(qname)
 
80
    sname = classmethod(sname)
 
81
 
 
82
    def __init__(self, *children, **attributes):
 
83
        super(WebDAVElement, self).__init__()
 
84
 
 
85
        if self.allowed_children is None:
 
86
            raise NotImplementedError("WebDAVElement subclass %s is not implemented."
 
87
                                      % (self.__class__.__name__,))
 
88
 
 
89
        #
 
90
        # Validate that children are of acceptable types
 
91
        #
 
92
        allowed_children = dict([
 
93
            (child_type, list(limits))
 
94
            for child_type, limits
 
95
            in self.allowed_children.items()
 
96
        ])
 
97
 
 
98
        my_children = []
 
99
 
 
100
        for child in children:
 
101
            if child is None:
 
102
                continue
 
103
 
 
104
            if isinstance(child, (str, unicode)):
 
105
                child = PCDATAElement(child)
 
106
 
 
107
            assert isinstance(child, (WebDAVElement, PCDATAElement)), "Not an element: %r" % (child,)
 
108
 
 
109
            for allowed, (min, max) in allowed_children.items():
 
110
                if type(allowed) == type and isinstance(child, allowed):
 
111
                    qname = allowed
 
112
                elif child.qname() == allowed:
 
113
                    qname = allowed
 
114
                else:
 
115
                    continue
 
116
 
 
117
                if min is not None and min > 0:
 
118
                    min -= 1
 
119
                if max is not None:
 
120
                    assert max > 0, "Too many children of type %s for %s" % (child.sname(), self.sname())
 
121
                    max -= 1
 
122
                allowed_children[qname] = (min, max)
 
123
                my_children.append(child)
 
124
                break
 
125
            else:
 
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()))
 
129
 
 
130
        for qname, (min, max) in allowed_children.items():
 
131
            if min != 0:
 
132
                raise ValueError("Not enough children of type {%s}%s for %s"
 
133
                                 % (qname[0], qname[1], self.sname()))
 
134
 
 
135
        self.children = tuple(my_children)
 
136
 
 
137
        #
 
138
        # Validate that attributes have known names
 
139
        #
 
140
        my_attributes = {}
 
141
 
 
142
        if self.allowed_attributes:
 
143
            for name in attributes:
 
144
                if name in self.allowed_attributes:
 
145
                    my_attributes[name] = attributes[name]
 
146
                else:
 
147
                    log.msg("Attribute %s is unexpected and therefore ignored in %s element"
 
148
                            % (name, self.sname()))
 
149
    
 
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()))
 
154
 
 
155
        elif not isinstance(self, WebDAVUnknownElement):
 
156
            if attributes:
 
157
                log.msg("Attributes %s are unexpected and therefore ignored in %s element"
 
158
                        % (attributes.keys(), self.sname()))
 
159
 
 
160
        self.attributes = my_attributes
 
161
 
 
162
    def __str__(self):
 
163
        return self.sname()
 
164
 
 
165
    def __repr__(self):
 
166
        if hasattr(self, "attributes") and hasattr(self, "children"):
 
167
            return "<%s %r: %r>" % (self.sname(), self.attributes, self.children)
 
168
        else:
 
169
            return "<%s>" % (self.sname())
 
170
 
 
171
    def __eq__(self, other):
 
172
        if isinstance(other, WebDAVElement):
 
173
            return (
 
174
                self.name       == other.name       and
 
175
                self.namespace  == other.namespace  and
 
176
                self.attributes == other.attributes and
 
177
                self.children   == other.children
 
178
            )
 
179
        else:
 
180
            return NotImplemented
 
181
 
 
182
    def __ne__(self, other):
 
183
        return not self.__eq__(other)
 
184
 
 
185
    def __contains__(self, child):
 
186
        return child in self.children
 
187
 
 
188
    def writeXML(self, output):
 
189
        document = xml.dom.minidom.Document()
 
190
        self.addToDOM(document, None)
 
191
        PrintXML(document, stream=output)
 
192
 
 
193
    def toxml(self):
 
194
        output = StringIO.StringIO()
 
195
        self.writeXML(output)
 
196
        return output.getvalue()
 
197
 
 
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)
 
206
        return element
 
207
 
 
208
    def addToDOM(self, document, parent):
 
209
        element = self.element(document)
 
210
 
 
211
        if parent is None:
 
212
            document.appendChild(element)
 
213
        else:
 
214
            parent.appendChild(element)
 
215
 
 
216
        for child in self.children:
 
217
            if child:
 
218
                try:
 
219
                    child.addToDOM(document, element)
 
220
                except:
 
221
                    log.err("Unable to add child %r of element %s to DOM" % (child, self))
 
222
                    raise
 
223
 
 
224
    def childrenOfType(self, child_type):
 
225
        """
 
226
        Returns a list of children with the same qname as the given type.
 
227
        """
 
228
        if type(child_type) is tuple:
 
229
            qname = child_type
 
230
        else:
 
231
            qname = child_type.qname()
 
232
 
 
233
        return [ c for c in self.children if c.qname() == qname ]
 
234
 
 
235
    def childOfType(self, child_type):
 
236
        """
 
237
        Returns a child of the given type, if any, or None.
 
238
        Raises ValueError if more than one is found.
 
239
        """
 
240
        found = None
 
241
        for child in self.childrenOfType(child_type):
 
242
            if found:
 
243
                raise ValueError("Multiple %s elements found in %s" % (child_type.sname(), self.toxml()))
 
244
            found = child
 
245
        return found
 
246
 
 
247
class PCDATAElement (object):
 
248
    def sname(self): return "#PCDATA"
 
249
 
 
250
    qname = classmethod(sname)
 
251
    sname = classmethod(sname)
 
252
 
 
253
    def __init__(self, data):
 
254
        super(PCDATAElement, self).__init__()
 
255
 
 
256
        if data is None:
 
257
            data = ""
 
258
        elif type(data) is unicode:
 
259
            data = data.encode("utf-8")
 
260
        else:
 
261
            assert type(data) is str, ("PCDATA must be a string: %r" % (data,))
 
262
 
 
263
        self.data = data
 
264
 
 
265
    def __str__(self):
 
266
        return str(self.data)
 
267
 
 
268
    def __repr__(self):
 
269
        return "<%s: %r>" % (self.__class__.__name__, self.data)
 
270
 
 
271
    def __add__(self, other):
 
272
        if isinstance(other, PCDATAElement):
 
273
            return self.__class__(self.data + other.data)
 
274
        else:
 
275
            return self.__class__(self.data + other)
 
276
 
 
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
 
282
        else:
 
283
            return NotImplemented
 
284
 
 
285
    def __ne__(self, other):
 
286
        return not self.__eq__(other)
 
287
 
 
288
    def isWhitespace(self):
 
289
        for char in str(self):
 
290
            if char not in string.whitespace:
 
291
                return False
 
292
        return True
 
293
 
 
294
    def element(self, document):
 
295
        return document.createTextNode(self.data)
 
296
 
 
297
    def addToDOM(self, document, parent):
 
298
        try:
 
299
            parent.appendChild(self.element(document))
 
300
        except TypeError:
 
301
            log.err("Invalid PCDATA: %r" % (self.data,))
 
302
            raise
 
303
 
 
304
class WebDAVOneShotElement (WebDAVElement):
 
305
    """
 
306
    Element with exactly one WebDAVEmptyElement child and no attributes.
 
307
    """
 
308
    __singletons = {}
 
309
 
 
310
    def __new__(clazz, *children):
 
311
        child = None
 
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))
 
317
                child = next
 
318
            elif isinstance(next, PCDATAElement):
 
319
                pass
 
320
            else:
 
321
                raise ValueError("%s child is not a WebDAVEmptyElement instance: %s"
 
322
                                 % (clazz.__name__, next))
 
323
 
 
324
        if clazz not in WebDAVOneShotElement.__singletons:
 
325
            WebDAVOneShotElement.__singletons[clazz] = {
 
326
                child: WebDAVElement.__new__(clazz, children)
 
327
            }
 
328
        elif child not in WebDAVOneShotElement.__singletons[clazz]:
 
329
            WebDAVOneShotElement.__singletons[clazz][child] = (
 
330
                WebDAVElement.__new__(clazz, children)
 
331
            )
 
332
 
 
333
        return WebDAVOneShotElement.__singletons[clazz][child]
 
334
 
 
335
class WebDAVUnknownElement (WebDAVElement):
 
336
    """
 
337
    Placeholder for unknown element tag names.
 
338
    """
 
339
    allowed_children = {
 
340
        WebDAVElement: (0, None),
 
341
        PCDATAElement: (0, None),
 
342
    }
 
343
 
 
344
class WebDAVEmptyElement (WebDAVElement):
 
345
    """
 
346
    WebDAV element with no contents.
 
347
    """
 
348
    __singletons = {}
 
349
 
 
350
    def __new__(clazz, *args, **kwargs):
 
351
        assert not args
 
352
 
 
353
        if kwargs:
 
354
            return WebDAVElement.__new__(clazz, **kwargs)
 
355
        else:
 
356
            if clazz not in WebDAVEmptyElement.__singletons:
 
357
                WebDAVEmptyElement.__singletons[clazz] = (WebDAVElement.__new__(clazz))
 
358
            return WebDAVEmptyElement.__singletons[clazz]
 
359
 
 
360
    allowed_children = {}
 
361
 
 
362
    children = ()
 
363
 
 
364
class WebDAVTextElement (WebDAVElement):
 
365
    """
 
366
    WebDAV element containing PCDATA.
 
367
    """
 
368
    def fromString(clazz, string):
 
369
        if string is None:
 
370
            return clazz()
 
371
        elif isinstance(string, (str, unicode)):
 
372
            return clazz(PCDATAElement(string))
 
373
        else:
 
374
            return clazz(PCDATAElement(str(string)))
 
375
 
 
376
    fromString = classmethod(fromString)
 
377
 
 
378
    allowed_children = { PCDATAElement: (0, None) }
 
379
 
 
380
    def __str__(self):
 
381
        return "".join([c.data for c in self.children])
 
382
 
 
383
    def __repr__(self):
 
384
        content = str(self)
 
385
        if content:
 
386
            return "<%s: %r>" % (self.sname(), content)
 
387
        else:
 
388
            return "<%s>" % (self.sname(),)
 
389
 
 
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
 
395
        else:
 
396
            return NotImplemented
 
397
 
 
398
class WebDAVDateTimeElement (WebDAVTextElement):
 
399
    """
 
400
    WebDAV date-time element. (RFC 2518, section 23.2)
 
401
    """
 
402
    def fromDate(clazz, date):
 
403
        """
 
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.
 
407
        """
 
408
        def isoformat(date):
 
409
            if date.utcoffset() is None:
 
410
                return date.isoformat() + "Z"
 
411
            else:
 
412
                return date.isoformat()
 
413
 
 
414
        if type(date) is int:
 
415
            date = isoformat(datetime.datetime.fromtimestamp(date))
 
416
        elif type(date) is str:
 
417
            pass
 
418
        elif type(date) is unicode:
 
419
            date = date.encode("utf-8")
 
420
        elif isinstance(date, datetime.datetime):
 
421
            date = isoformat(date)
 
422
        else:
 
423
            raise ValueError("Unknown date type: %r" % (date,))
 
424
 
 
425
        return clazz(PCDATAElement(date))
 
426
 
 
427
    fromDate = classmethod(fromDate)
 
428
 
 
429
    def __init__(self, *children, **attributes):
 
430
        super(WebDAVDateTimeElement, self).__init__(*children, **attributes)
 
431
        self.datetime() # Raise ValueError if the format is wrong
 
432
 
 
433
    def __eq__(self, other):
 
434
        if isinstance(other, WebDAVDateTimeElement):
 
435
            return self.datetime() == other.datetime()
 
436
        else:
 
437
            return NotImplemented
 
438
 
 
439
    def datetime(self):
 
440
        s = str(self)
 
441
        if not s:
 
442
            return None
 
443
        else:
 
444
            return parse_date(s)
 
445
 
 
446
class DateTimeHeaderElement (WebDAVTextElement):
 
447
    """
 
448
    WebDAV date-time element for elements that substitute for HTTP
 
449
    headers. (RFC 2068, section 3.3.1)
 
450
    """
 
451
    def fromDate(clazz, date):
 
452
        """
 
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.
 
456
        """
 
457
        def format(date):
 
458
            #
 
459
            # FIXME: strftime() is subject to localization nonsense; we need to
 
460
            # ensure that we're using the correct localization, or don't use
 
461
            # strftime().
 
462
            #
 
463
            return date.strftime("%a, %d %b %Y %H:%M:%S GMT")
 
464
 
 
465
        if type(date) is int:
 
466
            date = format(datetime.datetime.fromtimestamp(date))
 
467
        elif type(date) is str:
 
468
            pass
 
469
        elif type(date) is unicode:
 
470
            date = date.encode("utf-8")
 
471
        elif isinstance(date, datetime.datetime):
 
472
            if date.tzinfo:
 
473
                raise NotImplementedError("I need to normalize to UTC")
 
474
            date = format(date)
 
475
        else:
 
476
            raise ValueError("Unknown date type: %r" % (date,))
 
477
 
 
478
        return clazz(PCDATAElement(date))
 
479
 
 
480
    fromDate = classmethod(fromDate)
 
481
 
 
482
    def __init__(self, *children, **attributes):
 
483
        super(DateTimeHeaderElement, self).__init__(*children, **attributes)
 
484
        self.datetime() # Raise ValueError if the format is wrong
 
485
 
 
486
    def __eq__(self, other):
 
487
        if isinstance(other, WebDAVDateTimeElement):
 
488
            return self.datetime() == other.datetime()
 
489
        else:
 
490
            return NotImplemented
 
491
 
 
492
    def datetime(self):
 
493
        s = str(self)
 
494
        if not s:
 
495
            return None
 
496
        else:
 
497
            return parseDateTime(s)
 
498
 
 
499
##
 
500
# Utilities
 
501
##
 
502
 
 
503
class FixedOffset (datetime.tzinfo):
 
504
    """
 
505
    Fixed offset in minutes east from UTC.
 
506
    """
 
507
    def __init__(self, offset, name=None):
 
508
        super(FixedOffset, self).__init__()
 
509
 
 
510
        self._offset = datetime.timedelta(minutes=offset)
 
511
        self._name   = name
 
512
 
 
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)
 
516
 
 
517
def parse_date(date):
 
518
    """
 
519
    Parse an ISO 8601 date and return a corresponding datetime.datetime object.
 
520
    """
 
521
    # See http://www.iso.org/iso/en/prods-services/popstds/datesandtime.html
 
522
 
 
523
    global regex_date
 
524
 
 
525
    if regex_date is None:
 
526
        import re
 
527
 
 
528
        regex_date = re.compile(
 
529
            "^" +
 
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}))" +
 
533
            "$"
 
534
        )
 
535
 
 
536
    match = regex_date.match(date)
 
537
    if match is not None:
 
538
        subsecond = match.group("subsecond")
 
539
        if subsecond is None:
 
540
            subsecond = 0
 
541
        else:
 
542
            subsecond = int(subsecond)
 
543
 
 
544
        offset_sign = match.group("offset_sign")
 
545
        if offset_sign is None:
 
546
            offset = FixedOffset(0)
 
547
        else:
 
548
            offset_hour   = int(match.group("offset_hour"  ))
 
549
            offset_minute = int(match.group("offset_minute"))
 
550
 
 
551
            delta = (offset_hour * 60) + offset_minute
 
552
 
 
553
            if   offset_sign == "+": offset = FixedOffset(0 - delta)
 
554
            elif offset_sign == "-": offset = FixedOffset(0 + delta)
 
555
 
 
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")),
 
563
            subsecond,
 
564
            offset
 
565
        )
 
566
    else:
 
567
        raise ValueError("Invalid ISO 8601 date format: %r" % (date,))
 
568
 
 
569
regex_date = None