~ubuntu-branches/ubuntu/lucid/twisted-web2/lucid

« 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: 2006-02-23 00:38:42 UTC
  • mfrom: (0.1.2 upstream)
  • Revision ID: james.westby@ubuntu.com-20060223003842-rcpl8v09a91wfpvr
Tags: 0.1.0.20060222-1ubuntu1
Synchronize with Debian unstable.

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