# -*- coding: utf-8 -*-
'''\
:mod:`ecoxipy.validation` - Validating XML
==========================================
This module provides the :class:`ecoxipy.Output` implementation
:class:`ValidationOutputWrapper`, which validates the XML created using a
validator object. The class :class:`ListValidator` is a validator
implementation working based on black- or whitelists.
To use validation for markup builders, use instances of this class:
.. autoclass:: ValidationOutputWrapper
:no-members:
Validators should throw the following exception:
.. autoclass:: ValidationError
A simple black- or whitelist validator:
.. autoclass:: ListValidator
:no-members:
.. _ecoxipy.validation.examples:
Examples
--------
First we define a blacklist validator and use it to define a markup builder:
>>> blacklist = ListValidator(['script', 'style'], ['onclick', 'style'], ['xml-stylesheet'])
>>> from ecoxipy import MarkupBuilder
>>> from ecoxipy.string_output import StringOutput
>>> output = ValidationOutputWrapper(StringOutput(), blacklist)
>>> b = MarkupBuilder(output)
Here we create XML, invalid nodes are not created:
>>> print(b.p(
... b['xml-stylesheet': 'href="BadStyling.css"'],
... b['my-pi': 'info'],
... b.script('InsecureCode();'),
... 'Hello ', b.em(
... 'World', {'class': 'important', 'onclick': 'MalicousThings();'}),
... '!'
... ))
<p><?my-pi info?>Hello <em class="important">World</em>!</p>
And now we define a whitelist validator, which is not silent but raisese
exeptions on validation errors:
>>> whitelist = ListValidator(['p', 'em'], ['class'], ['my-pi'], False, False)
>>> output = ValidationOutputWrapper(StringOutput(), whitelist)
>>> b = MarkupBuilder(output)
First we create valid XML, then some invalid XML:
>>> print(b.p(
... b['my-pi': 'info'],
... 'Hello ', b.em('World', {'class': 'important'}), '!'
... ))
<p><?my-pi info?>Hello <em class="important">World</em>!</p>
>>> try:
... b['xml-stylesheet': 'href="BadStyling.css"']
... except ValidationError as e:
... print(e)
The processing instruction target "xml-stylesheet" is not allowed.
>>> try:
... b.script('InsecureCode();')
... except ValidationError as e:
... print(e)
The element name "script" is not allowed.
>>> try:
... b.em('World', {'class': 'important', 'onclick': 'MalicousThings();'})
... except ValidationError as e:
... print(e)
The attribute name "onclick" is not allowed.
If one of the element, attribute or processing instruction validity lists of
:class:`ListValidator` is :const:`None`, it allows all nodes of that type:
>>> anything = ListValidator()
>>> output = ValidationOutputWrapper(StringOutput(), anything)
>>> b = MarkupBuilder(output)
>>> print(b.p(
... b['my-pi': 'info'],
... 'Hello ', b.em('World', {'class': 'important'}), '!'
... ))
<p><?my-pi info?>Hello <em class="important">World</em>!</p>
>>> anything = ListValidator(blacklist=False)
>>> output = ValidationOutputWrapper(StringOutput(), anything)
>>> b = MarkupBuilder(output)
>>> print(b.p(
... b['my-pi': 'info'],
... 'Hello ', b.em('World', {'class': 'important'}), '!'
... ))
<p><?my-pi info?>Hello <em class="important">World</em>!</p>
'''
from ecoxipy import _unicode
[docs]class ValidationOutputWrapper(object):
'''\
Instances of this class wrap an :class:`ecoxipy.Output` instance and a
validator instance, the latter having a method like
:class:`ecoxipy.Output` for each XML node type it wishes to validate (i.e.
:meth:`element`, :meth:`text`, :meth:`comment`,
:meth:`processing_instruction` and :meth:`document`).
When a XML node is to be created using this class, first the appropriate
validator method is called. This might raise an exception to stop building
completely. If this returns :const:`None` or :const:`True`, the result of
calling the same method on the output instance is returned. Otherwise
the creation call returns :const:`None` to create nothing.
Note that a validator's :meth:`element` method receives the attributes
dictionary which is given to the output, thus changes made by a validator
are reflected in the created XML representation.
'''
def __init__(self, output, validator):
self._output = output
self._validator = validator
self._ValidationMethod(self, 'element')
self._ValidationMethod(self, 'text')
self._ValidationMethod(self, 'comment')
self._ValidationMethod(self, 'processing_instruction')
self._ValidationMethod(self, 'document')
try:
self.preprocess = output.preprocess
except AttributeError:
pass
class _ValidationMethod(object):
def __init__(self, wrapper, name):
try:
self._validation_method = getattr(wrapper._validator, name)
except AttributeError:
self._validation_method = None
self._creation_method = getattr(wrapper._output, name)
setattr(wrapper, name, self)
def __call__(self, *args):
if self._validation_method is None:
validation_result = None
else:
validation_result = self._validation_method(*args)
if validation_result is None or validation_result is True:
return self._creation_method(*args)
def is_native_type(self, content):
return self._output.is_native_type(content)
[docs]class ValidationError(Exception):
'''\
Should be raised by validators to indicate a error while validating, the
message should describe the problem.
'''
[docs]class ListValidator(object):
'''\
A simple black- or whitelist-based validator class (see
:class:`ValidationOutputWrapper`). It takes lists of element as well as
attribute names and processing instruction targets, all given names and
targets are converted to Unicode. If the ``blacklist`` argument is
:const:`True` the lists define which elements, attributes and processing
instructions are invalid. If ``blacklist`` is :const:`False` the instance
works as a whitelist, thus the lists define the valid elements, attributes
and processing instructions. If the argument ``silent`` is :const:`True`,
the validating methods return :const:`False` on validation errors,
otherwise they raise a :class:`ValidationError`.
:param element_names: An iterable of element names or :const:`None` to
accept all elements.
:param attribute_names: An iterable of attribute names or :const:`None` to
accept all attributes.
:param pi_targets: An iterable of processing instruction targets or
:const:`None` to accept all processing instructions.
:param blacklist: If this is :const:`True`, the instance works as a
blacklist, otherwise as a whitelist.
:type blacklist: :class:`bool`
:param silent: If this is :const:`True`, failed validations return
:const:`False` for invalid element names or processing instruction
targets and invalid attributes are deleted. Otherwise they raise a
:class:`ValidationError`.
:type silent: :class:`bool`
'''
def __init__(self, element_names=None, attribute_names=None,
pi_targets=None, blacklist=True, silent=True):
if bool(blacklist):
self._invalid = self._blacklist_invalid
none_container = self._NothingContainer
else:
self._invalid = self._whitelist_invalid
none_container = self._EverythingContainer
create_set = lambda items: (none_container if items is None
else {_unicode(item) for item in items})
self._element_names = create_set(element_names)
self._attribute_names = create_set(attribute_names)
self._pi_targets = create_set(pi_targets)
self._silent = bool(silent)
@staticmethod
def _whitelist_invalid(item, allowed):
return item not in allowed
@staticmethod
def _blacklist_invalid(item, forbidden):
return item in forbidden
class _EverythingContainer(object):
def __contains__(self, item):
return True
_EverythingContainer = _EverythingContainer()
class _NothingContainer(object):
def __contains__(self, item):
return False
_NothingContainer = _NothingContainer()
def element(self, name, children, attributes):
if self._invalid(name, self._element_names):
if self._silent:
return False
raise ValidationError(
'The element name "{}" is not allowed.'.format(name))
for attr_name in list(attributes.keys()):
if self._invalid(attr_name, self._attribute_names):
if self._silent:
del attributes[attr_name]
else:
raise ValidationError(
'The attribute name "{}" is not allowed.'.format(
attr_name))
def processing_instruction(self, target, content):
if self._invalid(target, self._pi_targets):
if self._silent:
return False
raise ValidationError(
'The processing instruction target "{}" is not allowed.'.format(
target))