5952965466c9a31ca865fd875895cbd09c2403c0
[platform/upstream/python-lxml.git] / src / lxml / dtd.pxi
1 # support for DTD validation
2 from lxml.includes cimport dtdvalid
3
4 cdef class DTDError(LxmlError):
5     """Base class for DTD errors.
6     """
7
8 cdef class DTDParseError(DTDError):
9     """Error while parsing a DTD.
10     """
11
12 cdef class DTDValidateError(DTDError):
13     """Error while validating an XML document with a DTD.
14     """
15
16
17 cdef inline int _assertValidDTDNode(node, void *c_node) except -1:
18     assert c_node is not NULL, u"invalid DTD proxy at %s" % id(node)
19
20
21 @cython.final
22 @cython.internal
23 @cython.freelist(8)
24 cdef class _DTDElementContentDecl:
25     cdef DTD _dtd
26     cdef tree.xmlElementContent* _c_node
27
28     def __repr__(self):
29         return "<%s.%s object name=%r type=%r occur=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.name, self.type, self.occur, id(self))
30
31     @property
32     def name(self):
33        _assertValidDTDNode(self, self._c_node)
34        return funicodeOrNone(self._c_node.name)
35
36     @property
37     def type(self):
38        _assertValidDTDNode(self, self._c_node)
39        cdef int type = self._c_node.type
40        if type == tree.XML_ELEMENT_CONTENT_PCDATA:
41            return "pcdata"
42        elif type == tree.XML_ELEMENT_CONTENT_ELEMENT:
43            return "element"
44        elif type == tree.XML_ELEMENT_CONTENT_SEQ:
45            return "seq"
46        elif type == tree.XML_ELEMENT_CONTENT_OR:
47            return "or"
48        else:
49            return None
50
51     @property
52     def occur(self):
53        _assertValidDTDNode(self, self._c_node)
54        cdef int occur = self._c_node.ocur
55        if occur == tree.XML_ELEMENT_CONTENT_ONCE:
56            return "once"
57        elif occur == tree.XML_ELEMENT_CONTENT_OPT:
58            return "opt"
59        elif occur == tree.XML_ELEMENT_CONTENT_MULT:
60            return "mult"
61        elif occur == tree.XML_ELEMENT_CONTENT_PLUS:
62            return "plus"
63        else:
64            return None
65
66     @property
67     def left(self):
68        _assertValidDTDNode(self, self._c_node)
69        c1 = self._c_node.c1
70        if c1:
71            node = <_DTDElementContentDecl>_DTDElementContentDecl.__new__(_DTDElementContentDecl)
72            node._dtd = self._dtd
73            node._c_node = <tree.xmlElementContent*>c1
74            return node
75        else:
76            return None
77
78     @property
79     def right(self):
80        _assertValidDTDNode(self, self._c_node)
81        c2 = self._c_node.c2
82        if c2:
83            node = <_DTDElementContentDecl>_DTDElementContentDecl.__new__(_DTDElementContentDecl)
84            node._dtd = self._dtd
85            node._c_node = <tree.xmlElementContent*>c2
86            return node
87        else:
88            return None
89
90
91 @cython.final
92 @cython.internal
93 @cython.freelist(8)
94 cdef class _DTDAttributeDecl:
95     cdef DTD _dtd
96     cdef tree.xmlAttribute* _c_node
97
98     def __repr__(self):
99         return "<%s.%s object name=%r elemname=%r prefix=%r type=%r default=%r default_value=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.name, self.elemname, self.prefix, self.type, self.default, self.default_value, id(self))
100
101     @property
102     def name(self):
103        _assertValidDTDNode(self, self._c_node)
104        return funicodeOrNone(self._c_node.name)
105
106     @property
107     def elemname(self):
108        _assertValidDTDNode(self, self._c_node)
109        return funicodeOrNone(self._c_node.elem)
110
111     @property
112     def prefix(self):
113        _assertValidDTDNode(self, self._c_node)
114        return funicodeOrNone(self._c_node.prefix)
115
116     @property
117     def type(self):
118        _assertValidDTDNode(self, self._c_node)
119        cdef int type = self._c_node.atype
120        if type == tree.XML_ATTRIBUTE_CDATA:
121            return "cdata"
122        elif type == tree.XML_ATTRIBUTE_ID:
123            return "id"
124        elif type == tree.XML_ATTRIBUTE_IDREF:
125            return "idref"
126        elif type == tree.XML_ATTRIBUTE_IDREFS:
127            return "idrefs"
128        elif type == tree.XML_ATTRIBUTE_ENTITY:
129            return "entity"
130        elif type == tree.XML_ATTRIBUTE_ENTITIES:
131            return "entities"
132        elif type == tree.XML_ATTRIBUTE_NMTOKEN:
133            return "nmtoken"
134        elif type == tree.XML_ATTRIBUTE_NMTOKENS:
135            return "nmtokens"
136        elif type == tree.XML_ATTRIBUTE_ENUMERATION:
137            return "enumeration"
138        elif type == tree.XML_ATTRIBUTE_NOTATION:
139            return "notation"
140        else:
141            return None
142
143     @property
144     def default(self):
145        _assertValidDTDNode(self, self._c_node)
146        cdef int default = self._c_node.def_
147        if default == tree.XML_ATTRIBUTE_NONE:
148            return "none"
149        elif default == tree.XML_ATTRIBUTE_REQUIRED:
150            return "required"
151        elif default == tree.XML_ATTRIBUTE_IMPLIED:
152            return "implied"
153        elif default == tree.XML_ATTRIBUTE_FIXED:
154            return "fixed"
155        else:
156            return None
157
158     @property
159     def default_value(self):
160        _assertValidDTDNode(self, self._c_node)
161        return funicodeOrNone(self._c_node.defaultValue)
162
163     def itervalues(self):
164         _assertValidDTDNode(self, self._c_node)
165         cdef tree.xmlEnumeration *c_node = self._c_node.tree
166         while c_node is not NULL:
167             yield funicode(c_node.name)
168             c_node = c_node.next
169
170     def values(self):
171         return list(self.itervalues())
172
173
174 @cython.final
175 @cython.internal
176 @cython.freelist(8)
177 cdef class _DTDElementDecl:
178     cdef DTD _dtd
179     cdef tree.xmlElement* _c_node
180
181     def __repr__(self):
182         return "<%s.%s object name=%r prefix=%r type=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.name, self.prefix, self.type, id(self))
183
184     @property
185     def name(self):
186         _assertValidDTDNode(self, self._c_node)
187         return funicodeOrNone(self._c_node.name)
188
189     @property
190     def prefix(self):
191        _assertValidDTDNode(self, self._c_node)
192        return funicodeOrNone(self._c_node.prefix)
193
194     @property
195     def type(self):
196        _assertValidDTDNode(self, self._c_node)
197        cdef int type = self._c_node.etype
198        if type == tree.XML_ELEMENT_TYPE_UNDEFINED:
199            return "undefined"
200        elif type == tree.XML_ELEMENT_TYPE_EMPTY:
201            return "empty"
202        elif type == tree.XML_ELEMENT_TYPE_ANY:
203            return "any"
204        elif type == tree.XML_ELEMENT_TYPE_MIXED:
205            return "mixed"
206        elif type == tree.XML_ELEMENT_TYPE_ELEMENT:
207            return "element"
208        else:
209            return None
210
211     @property
212     def content(self):
213        _assertValidDTDNode(self, self._c_node)
214        cdef tree.xmlElementContent *content = self._c_node.content
215        if content:
216            node = <_DTDElementContentDecl>_DTDElementContentDecl.__new__(_DTDElementContentDecl)
217            node._dtd = self._dtd
218            node._c_node = content
219            return node
220        else:
221            return None
222
223     def iterattributes(self):
224         _assertValidDTDNode(self, self._c_node)
225         cdef tree.xmlAttribute *c_node = self._c_node.attributes
226         while c_node:
227             node = <_DTDAttributeDecl>_DTDAttributeDecl.__new__(_DTDAttributeDecl)
228             node._dtd = self._dtd
229             node._c_node = c_node
230             yield node
231             c_node = c_node.nexth
232
233     def attributes(self):
234         return list(self.iterattributes())
235
236
237 @cython.final
238 @cython.internal
239 @cython.freelist(8)
240 cdef class _DTDEntityDecl:
241     cdef DTD _dtd
242     cdef tree.xmlEntity* _c_node
243     def __repr__(self):
244         return "<%s.%s object name=%r at 0x%x>" % (self.__class__.__module__, self.__class__.__name__, self.name, id(self))
245
246     @property
247     def name(self):
248         _assertValidDTDNode(self, self._c_node)
249         return funicodeOrNone(self._c_node.name)
250
251     @property
252     def orig(self):
253         _assertValidDTDNode(self, self._c_node)
254         return funicodeOrNone(self._c_node.orig)
255
256     @property
257     def content(self):
258         _assertValidDTDNode(self, self._c_node)
259         return funicodeOrNone(self._c_node.content)
260
261
262 ################################################################################
263 # DTD
264
265 cdef class DTD(_Validator):
266     u"""DTD(self, file=None, external_id=None)
267     A DTD validator.
268
269     Can load from filesystem directly given a filename or file-like object.
270     Alternatively, pass the keyword parameter ``external_id`` to load from a
271     catalog.
272     """
273     cdef tree.xmlDtd* _c_dtd
274     def __init__(self, file=None, *, external_id=None):
275         _Validator.__init__(self)
276         if file is not None:
277             if _isString(file):
278                 file = _encodeFilename(file)
279                 with self._error_log:
280                     self._c_dtd = xmlparser.xmlParseDTD(NULL, _xcstr(file))
281             elif hasattr(file, 'read'):
282                 self._c_dtd = _parseDtdFromFilelike(file)
283             else:
284                 raise DTDParseError, u"file must be a filename or file-like object"
285         elif external_id is not None:
286             with self._error_log:
287                 self._c_dtd = xmlparser.xmlParseDTD(<const_xmlChar*>external_id, NULL)
288         else:
289             raise DTDParseError, u"either filename or external ID required"
290
291         if self._c_dtd is NULL:
292             raise DTDParseError(
293                 self._error_log._buildExceptionMessage(u"error parsing DTD"),
294                 self._error_log)
295
296     @property
297     def name(self):
298        if self._c_dtd is NULL:
299            return None
300        return funicodeOrNone(self._c_dtd.name)
301
302     @property
303     def external_id(self):
304        if self._c_dtd is NULL:
305            return None
306        return funicodeOrNone(self._c_dtd.ExternalID)
307
308     @property
309     def system_url(self):
310        if self._c_dtd is NULL:
311            return None
312        return funicodeOrNone(self._c_dtd.SystemID)
313
314     def iterelements(self):
315         cdef tree.xmlNode *c_node = self._c_dtd.children if self._c_dtd is not NULL else NULL
316         while c_node is not NULL:
317             if c_node.type == tree.XML_ELEMENT_DECL:
318                 node = _DTDElementDecl()
319                 node._dtd = self
320                 node._c_node = <tree.xmlElement*>c_node
321                 yield node
322             c_node = c_node.next
323
324     def elements(self):
325         return list(self.iterelements())
326
327     def iterentities(self):
328         cdef tree.xmlNode *c_node = self._c_dtd.children if self._c_dtd is not NULL else NULL
329         while c_node is not NULL:
330             if c_node.type == tree.XML_ENTITY_DECL:
331                 node = _DTDEntityDecl()
332                 node._dtd = self
333                 node._c_node = <tree.xmlEntity*>c_node
334                 yield node
335             c_node = c_node.next
336
337     def entities(self):
338         return list(self.iterentities())
339
340     def __dealloc__(self):
341         tree.xmlFreeDtd(self._c_dtd)
342
343     def __call__(self, etree):
344         u"""__call__(self, etree)
345
346         Validate doc using the DTD.
347
348         Returns true if the document is valid, false if not.
349         """
350         cdef _Document doc
351         cdef _Element root_node
352         cdef xmlDoc* c_doc
353         cdef dtdvalid.xmlValidCtxt* valid_ctxt
354         cdef int ret = -1
355
356         assert self._c_dtd is not NULL, "DTD not initialised"
357         doc = _documentOrRaise(etree)
358         root_node = _rootNodeOrRaise(etree)
359
360         valid_ctxt = dtdvalid.xmlNewValidCtxt()
361         if valid_ctxt is NULL:
362             raise DTDError(u"Failed to create validation context")
363
364         # work around error reporting bug in libxml2 <= 2.9.1 (and later?)
365         # https://bugzilla.gnome.org/show_bug.cgi?id=724903
366         valid_ctxt.error = <dtdvalid.xmlValidityErrorFunc>_nullGenericErrorFunc
367         valid_ctxt.userData = NULL
368
369         try:
370             with self._error_log:
371                 c_doc = _fakeRootDoc(doc._c_doc, root_node._c_node)
372                 ret = dtdvalid.xmlValidateDtd(valid_ctxt, c_doc, self._c_dtd)
373                 _destroyFakeDoc(doc._c_doc, c_doc)
374         finally:
375             dtdvalid.xmlFreeValidCtxt(valid_ctxt)
376
377         if ret == -1:
378             raise DTDValidateError(u"Internal error in DTD validation",
379                                    self._error_log)
380         return ret == 1
381
382
383 cdef tree.xmlDtd* _parseDtdFromFilelike(file) except NULL:
384     cdef _ExceptionContext exc_context
385     cdef _FileReaderContext dtd_parser
386     cdef _ErrorLog error_log
387     cdef tree.xmlDtd* c_dtd = NULL
388     exc_context = _ExceptionContext()
389     dtd_parser = _FileReaderContext(file, exc_context, None)
390     error_log = _ErrorLog()
391
392     with error_log:
393         c_dtd = dtd_parser._readDtd()
394
395     exc_context._raise_if_stored()
396     if c_dtd is NULL:
397         raise DTDParseError(u"error parsing DTD", error_log)
398     return c_dtd
399
400 cdef DTD _dtdFactory(tree.xmlDtd* c_dtd):
401     # do not run through DTD.__init__()!
402     cdef DTD dtd
403     if c_dtd is NULL:
404         return None
405     dtd = DTD.__new__(DTD)
406     dtd._c_dtd = _copyDtd(c_dtd)
407     _Validator.__init__(dtd)
408     return dtd
409
410
411 cdef tree.xmlDtd* _copyDtd(tree.xmlDtd* c_orig_dtd) except NULL:
412     """
413     Copy a DTD.  libxml2 (currently) fails to set up the element->attributes
414     links when copying DTDs, so we have to rebuild them here.
415     """
416     c_dtd = tree.xmlCopyDtd(c_orig_dtd)
417     if not c_dtd:
418         raise MemoryError
419     cdef tree.xmlNode* c_node = c_dtd.children
420     while c_node:
421         if c_node.type == tree.XML_ATTRIBUTE_DECL:
422             _linkDtdAttribute(c_dtd, <tree.xmlAttribute*>c_node)
423         c_node = c_node.next
424     return c_dtd
425
426
427 cdef void _linkDtdAttribute(tree.xmlDtd* c_dtd, tree.xmlAttribute* c_attr):
428     """
429     Create the link to the DTD attribute declaration from the corresponding
430     element declaration.
431     """
432     c_elem = dtdvalid.xmlGetDtdElementDesc(c_dtd, c_attr.elem)
433     if not c_elem:
434         # no such element? something is wrong with the DTD ...
435         return
436     c_pos = c_elem.attributes
437     if not c_pos:
438         c_elem.attributes = c_attr
439         c_attr.nexth = NULL
440         return
441     # libxml2 keeps namespace declarations first, and we need to make
442     # sure we don't re-insert attributes that are already there
443     if _isDtdNsDecl(c_attr):
444         if not _isDtdNsDecl(c_pos):
445             c_elem.attributes = c_attr
446             c_attr.nexth = c_pos
447             return
448         while c_pos != c_attr and c_pos.nexth and _isDtdNsDecl(c_pos.nexth):
449             c_pos = c_pos.nexth
450     else:
451         # append at end
452         while c_pos != c_attr and c_pos.nexth:
453             c_pos = c_pos.nexth
454     if c_pos == c_attr:
455         return
456     c_attr.nexth = c_pos.nexth
457     c_pos.nexth = c_attr
458
459
460 cdef bint _isDtdNsDecl(tree.xmlAttribute* c_attr):
461     if cstring_h.strcmp(<const_char*>c_attr.name, "xmlns") == 0:
462         return True
463     if (c_attr.prefix is not NULL and
464             cstring_h.strcmp(<const_char*>c_attr.prefix, "xmlns") == 0):
465         return True
466     return False