Ticket #14: xmlextra.2.py

File xmlextra.2.py, 16.5 KB (added by roberto ostinelli, 2 years ago)

suggested replacement for the xmlextra.py file

Line 
1#
2# (C) Copyright 2003-2006 Jacek Konieczny <jajcus@jajcus.net>
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU Lesser General Public License Version
6# 2.1 as published by the Free Software Foundation.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11# GNU Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public
14# License along with this program; if not, write to the Free Software
15# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
16#
17# pylint: disable-msg=C0103, W0132, W0611
18
19"""Extension to libxml2 for XMPP stream and stanza processing"""
20
21__revision__="$Id: xmlextra.py,v 1.15 2004/10/11 18:33:51 jajcus Exp $"
22__docformat__="restructuredtext en"
23
24import sys
25import libxml2
26import threading
27import re
28
29from pyxmpp.exceptions import StreamParseError
30
31common_doc = libxml2.newDoc("1.0")
32common_root = common_doc.newChild(None,"root",None)
33COMMON_NS = "http://pyxmpp.jajcus.net/xmlns/common"
34common_ns = common_root.newNs(COMMON_NS, None)
35common_root.setNs(common_ns)
36common_doc.setRootElement(common_root)
37
38class StreamHandler:
39    """Base class for stream handler."""
40    def __init__(self):
41        pass
42
43    def _stream_start(self,_doc):
44        """Process stream start."""
45        doc=libxml2.xmlDoc(_doc)
46        self.stream_start(doc)
47
48    def _stream_end(self,_doc):
49        """Process stream end."""
50        doc=libxml2.xmlDoc(_doc)
51        self.stream_end(doc)
52
53    def _stanza(self,_doc,_node):
54        """Process complete stanza."""
55        doc=libxml2.xmlDoc(_doc)
56        node=libxml2.xmlNode(_node)
57        self.stanza(doc,node)
58
59    def stream_start(self,doc):
60        """Called when the start tag of root element is encountered
61        in the stream.
62
63        :Parameters:
64            - `doc`: the document being parsed.
65        :Types:
66            - `doc`: `libxml2.xmlDoc`"""
67        print >>sys.stderr,"Unhandled stream start:",`doc.serialize()`
68
69    def stream_end(self,doc):
70        """Called when the end tag of root element is encountered
71        in the stream.
72
73        :Parameters:
74            - `doc`: the document being parsed.
75        :Types:
76            - `doc`: `libxml2.xmlDoc`"""
77        print >>sys.stderr,"Unhandled stream end",`doc.serialize()`
78
79    def stanza(self, _unused, node):
80        """Called when the end tag of a direct child of the root
81        element is encountered in the stream.
82
83        Please note, that node will be removed from the document
84        and freed after this method returns. If it is needed after
85        that a copy must be made before the method returns.
86
87        :Parameters:
88            - `_unused`: the document being parsed.
89            - `node`: the (complete) element being processed
90        :Types:
91            - `_unused`: `libxml2.xmlDoc`
92            - `node`: `libxml2.xmlNode`"""
93        print >>sys.stderr,"Unhandled stanza",`node.serialize()`
94
95    def error(self,descr):
96        """Called when an error is encountered in the stream.
97
98        :Parameters:
99            - `descr`: description of the error
100        :Types:
101            - `descr`: `str`"""
102        raise StreamParseError,descr
103
104try:
105#########################################################################
106# C-extension based workarounds for libxml2 limitations
107#-------------------------------------------------------
108    from pyxmpp import _xmlextra
109    from pyxmpp._xmlextra import error
110
111    _create_reader = _xmlextra.sax_reader_new
112
113    def replace_ns(node, old_ns,new_ns):
114        """Replace namespaces in a whole subtree.
115
116        The old namespace declaration will be removed if present on the `node`.
117
118        :Parameters:
119           - `node`: the root of the subtree where namespaces should be replaced.
120           - `old_ns`: the namespace to replace.
121           - `new_ns`: the namespace to be used instead of old_ns.
122        :Types:
123            - `node`: `libxml2.xmlNode`
124            - `old_ns`: `libxml2.xmlNs`
125            - `new_ns`: `libxml2.xmlNs`
126
127        Both old_ns and new_ns may be None meaning no namespace set."""
128        if old_ns is None:
129            old_ns__o = None
130        else:
131            old_ns__o = old_ns._o
132        if new_ns is None:
133            new_ns__o = None
134        else:
135            new_ns__o = new_ns._o
136        if node is None:
137            node__o = None
138        else:
139            node__o = node._o
140        _xmlextra.replace_ns(node__o, old_ns__o, new_ns__o)
141        if old_ns__o:
142            _xmlextra.remove_ns(node__o, old_ns__o)
143
144    pure_python = False
145
146except ImportError:
147#########################################################################
148# Pure python implementation (slow workarounds for libxml2 limitations)
149#-----------------------------------------------------------------------
150    class error(Exception):
151        """Exception raised on a stream parse error."""
152        pass
153
154    def _escape(data):
155        """Escape data for XML"""
156        data=data.replace("&","&amp;")
157        data=data.replace("<","&lt;")
158        data=data.replace(">","&gt;")
159        data=data.replace("'","&apos;")
160        data=data.replace('"',"&quot;")
161        return data
162
163    class _SAXCallback(libxml2.SAXCallback):
164        """SAX events handler for the python-only stream parser."""
165        def __init__(self, handler):
166            """Initialize the SAX handler.
167
168            :Parameters:
169                - `handler`: Object to handle stream start, end and stanzas.
170            :Types:
171                - `handler`: `StreamHandler`
172            """
173            self._handler = handler
174            self._head = ""
175            self._tail = ""
176            self._current = ""
177            self._level = 0
178            self._doc = None
179            self._root = None
180
181        def cdataBlock(self, data):
182            ""
183            if self._level>1:
184                self._current += _escape(data)
185
186        def characters(self, data):
187            ""
188            if self._level>1:
189                self._current += _escape(data)
190
191        def comment(self, content):
192            ""
193            pass
194
195        def endDocument(self):
196            ""
197            pass
198
199        def endElement(self, tag):
200            ""
201            self._current+="</%s>" % (tag,)
202            self._level -= 1
203            if self._level > 1:
204                return
205            if self._level==1:
206                xml=self._head+self._current+self._tail
207                doc=libxml2.parseDoc(xml)
208                try:
209                    node = doc.getRootElement().children
210                except:
211                    doc.freeDoc()
212                    return
213                try:
214                    node1 = node.docCopyNode(self._doc, 1)
215                except:
216                    del node
217                    doc.freeDoc()
218                    return
219                try:
220                    self._root.addChild(node1)
221                except:
222                    node1.unlinkNode()
223                    node1.freeNode()
224                    del node1
225                    del node
226                    doc.freeDoc()
227                try:
228                    self._handler.stanza(self._doc, node1)
229                except:
230                    node1.unlinkNode()
231                    node1.freeNode()
232                    del node1
233                    del node
234                    doc.freeDoc()
235                    raise
236                del node1
237                del node
238                doc.freeDoc()
239            else:
240                xml=self._head+self._tail
241                doc=libxml2.parseDoc(xml)
242                try:
243                    self._handler.stream_end(self._doc)
244                    self._doc.freeDoc()
245                    self._doc = None
246                    self._root = None
247                finally:
248                    doc.freeDoc()
249
250        def error(self, msg):
251            ""
252            self._handler.error(msg)
253
254        fatalError = error
255
256        ignorableWhitespace = characters
257
258        def reference(self, name):
259            ""
260            self._current += "&" + name + ";"
261
262        def startDocument(self):
263            ""
264            pass
265
266        def startElement(self, tag, attrs):
267            ""
268            s = "<"+tag
269            if attrs:
270                for a,v in attrs.items():
271                    s+=" %s='%s'" % (a,_escape(v))
272            s += ">"
273            if self._level == 0:
274                self._head = s
275                self._tail = "</%s>" % (tag,)
276                xml=self._head+self._tail
277                self._doc = libxml2.parseDoc(xml)
278                self._handler.stream_start(self._doc)
279                self._root = self._doc.getRootElement()
280            elif self._level == 1:
281                self._current = s
282            else:
283                self._current += s
284            self._level += 1
285
286        def warning(self):
287            ""
288            pass
289
290    class _PythonReader:
291        """Python-only stream reader."""
292        def __init__(self,handler):
293            """Initialize the reader.
294
295            :Parameters:
296                - `handler`: Object to handle stream start, end and stanzas.
297            :Types:
298                - `handler`: `StreamHandler`
299            """
300            self.handler = handler
301            self.sax = _SAXCallback(handler)
302            self.parser = libxml2.createPushParser(self.sax, '', 0, 'stream')
303
304        def feed(self, data):
305            """Feed the parser with a chunk of data. Apropriate methods
306            of `self.handler` will be called whenever something interesting is
307            found.
308
309            :Parameters:
310                - `data`: the chunk of data to parse.
311            :Types:
312                - `data`: `str`"""
313            return self.parser.parseChunk(data, len(data), 0)
314
315    _create_reader = _PythonReader
316
317    def _get_ns(node):
318        """Get namespace of node.
319
320        :return: the namespace object or `None` if the node has no namespace
321        assigned.
322        :returntype: `libxml2.xmlNs`"""
323        try:
324            return node.ns()
325        except libxml2.treeError:
326            return None
327
328    def replace_ns(node, old_ns, new_ns):
329        """Replace namespaces in a whole subtree.
330
331        :Parameters:
332           - `node`: the root of the subtree where namespaces should be replaced.
333           - `old_ns`: the namespace to replace.
334           - `new_ns`: the namespace to be used instead of old_ns.
335        :Types:
336            - `node`: `libxml2.xmlNode`
337            - `old_ns`: `libxml2.xmlNs`
338            - `new_ns`: `libxml2.xmlNs`
339
340        Both old_ns and new_ns may be None meaning no namespace set."""
341
342        if old_ns is not None:
343            old_ns_uri = old_ns.content
344            old_ns_prefix = old_ns.name
345        else:
346            old_ns_uri = None
347            old_ns_prefix = None
348
349        ns = _get_ns(node)
350        if ns is None and old_ns is None:
351            node.setNs(new_ns)
352        elif ns and ns.content == old_ns_uri and ns.name == old_ns_prefix:
353            node.setNs(new_ns)
354
355        p = node.properties
356        while p:
357            ns = _get_ns(p)
358            if ns is None and old_ns is None:
359                p.setNs(new_ns)
360            if ns and ns.content == old_ns_uri and ns.name == old_ns_prefix:
361                p.setNs(new_ns)
362            p = p.next
363
364        n = node.children
365        while n:
366            if n.type == 'element':
367                skip_element = False
368                try:
369                    nsd = n.nsDefs()
370                except libxml2.treeError:
371                    nsd = None
372                while nsd:
373                    if nsd.name == old_ns_prefix:
374                        skip_element = True
375                        break
376                    nsd = nsd.next
377                if not skip_element:
378                    replace_ns(n, old_ns, new_ns)
379            n = n.next
380
381    pure_python = True
382
383###########################################################
384# Common code
385#-------------
386
387def get_node_ns(xmlnode):
388    """Namespace of an XML node.
389
390    :Parameters:
391        - `xmlnode`: the XML node to query.
392    :Types:
393        - `xmlnode`: `libxml2.xmlNode`
394
395    :return: namespace of the node or `None`
396    :returntype: `libxml2.xmlNs`"""
397    try:
398        return xmlnode.ns()
399    except libxml2.treeError:
400        return None
401
402def get_node_ns_uri(xmlnode):
403    """Return namespace URI of an XML node.
404
405    :Parameters:
406        - `xmlnode`: the XML node to query.
407    :Types:
408        - `xmlnode`: `libxml2.xmlNode`
409
410    :return: namespace URI of the node or `None`
411    :returntype: `unicode`"""
412    ns=get_node_ns(xmlnode)
413    if ns:
414        return unicode(ns.getContent(),"utf-8")
415    else:
416        return None
417
418def xml_node_iter(nodelist):
419    """Iterate over sibling XML nodes. All types of nodes will be returned
420    (not only the elements).
421
422    Usually used to iterade over node's children like this::
423
424        xml_node_iter(node.children)
425
426    :Parameters:
427        - `nodelist`: start node of the list.
428    :Types:
429        - `nodelist`: `libxml2.xmlNode`
430    """
431    node = nodelist
432    while node:
433        yield node
434        node = node.next
435
436def xml_element_iter(nodelist):
437    """Iterate over sibling XML elements. Non-element nodes will be skipped.
438
439    Usually used to iterade over node's children like this::
440
441        xml_node_iter(node.children)
442
443    :Parameters:
444        - `nodelist`: start node of the list.
445    :Types:
446        - `nodelist`: `libxml2.xmlNode`
447    """
448    node = nodelist
449    while node:
450        if node.type == "element":
451            yield node
452        node = node.next
453
454def xml_element_ns_iter(nodelist, ns_uri):
455    """Iterate over sibling XML elements. Only elements in the given namespace will be returned.
456
457    Usually used to iterade over node's children like this::
458
459        xml_node_iter(node.children)
460
461    :Parameters:
462        - `nodelist`: start node of the list.
463    :Types:
464        - `nodelist`: `libxml2.xmlNode`
465    """
466    node = nodelist
467    while node:
468        if node.type == "element" and get_node_ns_uri(node)==ns_uri:
469            yield node
470        node = node.next
471
472evil_characters_re=re.compile(r"[\000-\010\013\014\016-\037]",re.UNICODE)
473utf8_replacement_char=u"\ufffd".encode("utf-8")
474
475def remove_evil_characters(s):
476    """Remove control characters (not allowed in XML) from a string."""
477    if isinstance(s,unicode):
478        return evil_characters_re.sub(u"\ufffd",s)
479    else:
480        return evil_characters_re.sub(utf8_replacement_char,s)
481
482bad_nsdef_replace_re=re.compile(r"^([^<]*\<[^><]*\s+)(xmlns=((\"[^\"]*\")|(\'[^\']*\')))")
483
484def safe_serialize(xmlnode):
485    """Serialize an XML element making sure the result is sane.
486
487    Remove control characters and invalid namespace declarations from the
488    result string.
489
490    :Parameters:
491        - `xmlnode`: the XML element to serialize.
492    :Types:
493        - `xmlnode`: `libxml2.xmlNode`
494
495    :return: UTF-8 encoded serialized and sanitized element.
496    :returntype: `string`"""
497    try:
498        ns = xmlnode.ns()
499    except libxml2.treeError:
500        ns = None
501    try:
502        nsdef = xmlnode.nsDefs()
503    except libxml2.treeError:
504        nsdef = None
505    s=xmlnode.serialize(encoding="UTF-8")
506    while nsdef:
507        if nsdef.name is None and (not ns or (nsdef.name, nsdef.content)!=(ns.name, ns.content)):
508            s = bad_nsdef_replace_re.sub("\\1",s,1)
509            break
510        nsdef = nsdef.next
511    s=remove_evil_characters(s)
512    return s
513
514class StreamReader:
515    """A simple push-parser interface for XML streams."""
516    def __init__(self,handler):
517        """Initialize `StreamReader` object.
518
519        :Parameters:
520            - `handler`: handler object for the stream content
521        :Types:
522            - `handler`: `StreamHandler` derived class
523        """
524        self.reader=_create_reader(handler)
525        self.lock=threading.RLock()
526        self.in_use=0
527    def doc(self):
528        """Get the document being parsed.
529
530        :return: the document.
531        :returntype: `libxml2.xmlNode`"""
532        ret=self.reader.doc()
533        if ret:
534            return libxml2.xmlDoc(ret)
535        else:
536            return None
537    def feed(self,s):
538        """Pass a string to the stream parser.
539
540        Parameters:
541            - `s`: string to parse.
542        Types:
543            - `s`: `str`
544
545        :return: `None` on EOF, `False` when whole input was parsed and `True`
546            if there is something still left in the buffer."""
547        self.lock.acquire()
548        if self.in_use:
549            self.lock.release()
550            raise StreamParseError,"StreamReader.feed() is not reentrant!"
551        self.in_use=1
552
553        try:
554            return self.reader.feed(s)
555        finally:
556            self.in_use=0
557            self.lock.release()
558
559
560# vi: sts=4 et sw=4