Imported Upstream version 4.5.2
[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                     orig_loader = _register_document_loader()
281                     self._c_dtd = xmlparser.xmlParseDTD(NULL, _xcstr(file))
282                     _reset_document_loader(orig_loader)
283             elif hasattr(file, 'read'):
284                 orig_loader = _register_document_loader()
285                 self._c_dtd = _parseDtdFromFilelike(file)
286                 _reset_document_loader(orig_loader)
287             else:
288                 raise DTDParseError, u"file must be a filename or file-like object"
289         elif external_id is not None:
290             with self._error_log:
291                 orig_loader = _register_document_loader()
292                 self._c_dtd = xmlparser.xmlParseDTD(<const_xmlChar*>external_id, NULL)
293                 _reset_document_loader(orig_loader)
294         else:
295             raise DTDParseError, u"either filename or external ID required"
296
297         if self._c_dtd is NULL:
298             raise DTDParseError(
299                 self._error_log._buildExceptionMessage(u"error parsing DTD"),
300                 self._error_log)
301
302     @property
303     def name(self):
304        if self._c_dtd is NULL:
305            return None
306        return funicodeOrNone(self._c_dtd.name)
307
308     @property
309     def external_id(self):
310        if self._c_dtd is NULL:
311            return None
312        return funicodeOrNone(self._c_dtd.ExternalID)
313
314     @property
315     def system_url(self):
316        if self._c_dtd is NULL:
317            return None
318        return funicodeOrNone(self._c_dtd.SystemID)
319
320     def iterelements(self):
321         cdef tree.xmlNode *c_node = self._c_dtd.children if self._c_dtd is not NULL else NULL
322         while c_node is not NULL:
323             if c_node.type == tree.XML_ELEMENT_DECL:
324                 node = _DTDElementDecl()
325                 node._dtd = self
326                 node._c_node = <tree.xmlElement*>c_node
327                 yield node
328             c_node = c_node.next
329
330     def elements(self):
331         return list(self.iterelements())
332
333     def iterentities(self):
334         cdef tree.xmlNode *c_node = self._c_dtd.children if self._c_dtd is not NULL else NULL
335         while c_node is not NULL:
336             if c_node.type == tree.XML_ENTITY_DECL:
337                 node = _DTDEntityDecl()
338                 node._dtd = self
339                 node._c_node = <tree.xmlEntity*>c_node
340                 yield node
341             c_node = c_node.next
342
343     def entities(self):
344         return list(self.iterentities())
345
346     def __dealloc__(self):
347         tree.xmlFreeDtd(self._c_dtd)
348
349     def __call__(self, etree):
350         u"""__call__(self, etree)
351
352         Validate doc using the DTD.
353
354         Returns true if the document is valid, false if not.
355         """
356         cdef _Document doc
357         cdef _Element root_node
358         cdef xmlDoc* c_doc
359         cdef dtdvalid.xmlValidCtxt* valid_ctxt
360         cdef int ret = -1
361
362         assert self._c_dtd is not NULL, "DTD not initialised"
363         doc = _documentOrRaise(etree)
364         root_node = _rootNodeOrRaise(etree)
365
366         valid_ctxt = dtdvalid.xmlNewValidCtxt()
367         if valid_ctxt is NULL:
368             raise DTDError(u"Failed to create validation context")
369
370         # work around error reporting bug in libxml2 <= 2.9.1 (and later?)
371         # https://bugzilla.gnome.org/show_bug.cgi?id=724903
372         valid_ctxt.error = <dtdvalid.xmlValidityErrorFunc>_nullGenericErrorFunc
373         valid_ctxt.userData = NULL
374
375         try:
376             with self._error_log:
377                 c_doc = _fakeRootDoc(doc._c_doc, root_node._c_node)
378                 ret = dtdvalid.xmlValidateDtd(valid_ctxt, c_doc, self._c_dtd)
379                 _destroyFakeDoc(doc._c_doc, c_doc)
380         finally:
381             dtdvalid.xmlFreeValidCtxt(valid_ctxt)
382
383         if ret == -1:
384             raise DTDValidateError(u"Internal error in DTD validation",
385                                    self._error_log)
386         return ret == 1
387
388
389 cdef tree.xmlDtd* _parseDtdFromFilelike(file) except NULL:
390     cdef _ExceptionContext exc_context
391     cdef _FileReaderContext dtd_parser
392     cdef _ErrorLog error_log
393     cdef tree.xmlDtd* c_dtd = NULL
394     exc_context = _ExceptionContext()
395     dtd_parser = _FileReaderContext(file, exc_context, None)
396     error_log = _ErrorLog()
397
398     with error_log:
399         c_dtd = dtd_parser._readDtd()
400
401     exc_context._raise_if_stored()
402     if c_dtd is NULL:
403         raise DTDParseError(u"error parsing DTD", error_log)
404     return c_dtd
405
406 cdef DTD _dtdFactory(tree.xmlDtd* c_dtd):
407     # do not run through DTD.__init__()!
408     cdef DTD dtd
409     if c_dtd is NULL:
410         return None
411     dtd = DTD.__new__(DTD)
412     dtd._c_dtd = _copyDtd(c_dtd)
413     _Validator.__init__(dtd)
414     return dtd
415
416
417 cdef tree.xmlDtd* _copyDtd(tree.xmlDtd* c_orig_dtd) except NULL:
418     """
419     Copy a DTD.  libxml2 (currently) fails to set up the element->attributes
420     links when copying DTDs, so we have to rebuild them here.
421     """
422     c_dtd = tree.xmlCopyDtd(c_orig_dtd)
423     if not c_dtd:
424         raise MemoryError
425     cdef tree.xmlNode* c_node = c_dtd.children
426     while c_node:
427         if c_node.type == tree.XML_ATTRIBUTE_DECL:
428             _linkDtdAttribute(c_dtd, <tree.xmlAttribute*>c_node)
429         c_node = c_node.next
430     return c_dtd
431
432
433 cdef void _linkDtdAttribute(tree.xmlDtd* c_dtd, tree.xmlAttribute* c_attr):
434     """
435     Create the link to the DTD attribute declaration from the corresponding
436     element declaration.
437     """
438     c_elem = dtdvalid.xmlGetDtdElementDesc(c_dtd, c_attr.elem)
439     if not c_elem:
440         # no such element? something is wrong with the DTD ...
441         return
442     c_pos = c_elem.attributes
443     if not c_pos:
444         c_elem.attributes = c_attr
445         c_attr.nexth = NULL
446         return
447     # libxml2 keeps namespace declarations first, and we need to make
448     # sure we don't re-insert attributes that are already there
449     if _isDtdNsDecl(c_attr):
450         if not _isDtdNsDecl(c_pos):
451             c_elem.attributes = c_attr
452             c_attr.nexth = c_pos
453             return
454         while c_pos != c_attr and c_pos.nexth and _isDtdNsDecl(c_pos.nexth):
455             c_pos = c_pos.nexth
456     else:
457         # append at end
458         while c_pos != c_attr and c_pos.nexth:
459             c_pos = c_pos.nexth
460     if c_pos == c_attr:
461         return
462     c_attr.nexth = c_pos.nexth
463     c_pos.nexth = c_attr
464
465
466 cdef bint _isDtdNsDecl(tree.xmlAttribute* c_attr):
467     if cstring_h.strcmp(<const_char*>c_attr.name, "xmlns") == 0:
468         return True
469     if (c_attr.prefix is not NULL and
470             cstring_h.strcmp(<const_char*>c_attr.prefix, "xmlns") == 0):
471         return True
472     return False