from __future__ import unicode_literals import re import uuid import warnings from wtforms.compat import string_types, text_type __all__ = ( 'DataRequired', 'data_required', 'Email', 'email', 'EqualTo', 'equal_to', 'IPAddress', 'ip_address', 'InputRequired', 'input_required', 'Length', 'length', 'NumberRange', 'number_range', 'Optional', 'optional', 'Required', 'required', 'Regexp', 'regexp', 'URL', 'url', 'AnyOf', 'any_of', 'NoneOf', 'none_of', 'MacAddress', 'mac_address', 'UUID', 'ValidationError', 'StopValidation' ) class ValidationError(ValueError): """ Raised when a validator fails to validate its input. """ def __init__(self, message='', *args, **kwargs): ValueError.__init__(self, message, *args, **kwargs) class StopValidation(Exception): """ Causes the validation chain to stop. If StopValidation is raised, no more validators in the validation chain are called. If raised with a message, the message will be added to the errors list. """ def __init__(self, message='', *args, **kwargs): Exception.__init__(self, message, *args, **kwargs) class EqualTo(object): """ Compares the values of two fields. :param fieldname: The name of the other field to compare to. :param message: Error message to raise in case of a validation error. Can be interpolated with `%(other_label)s` and `%(other_name)s` to provide a more helpful error. """ def __init__(self, fieldname, message=None): self.fieldname = fieldname self.message = message def __call__(self, form, field): try: other = form[self.fieldname] except KeyError: raise ValidationError(field.gettext("Invalid field name '%s'.") % self.fieldname) if field.data != other.data: d = { 'other_label': hasattr(other, 'label') and other.label.text or self.fieldname, 'other_name': self.fieldname } message = self.message if message is None: message = field.gettext('Field must be equal to %(other_name)s.') raise ValidationError(message % d) class Length(object): """ Validates the length of a string. :param min: The minimum required length of the string. If not provided, minimum length will not be checked. :param max: The maximum length of the string. If not provided, maximum length will not be checked. :param message: Error message to raise in case of a validation error. Can be interpolated using `%(min)d` and `%(max)d` if desired. Useful defaults are provided depending on the existence of min and max. """ def __init__(self, min=-1, max=-1, message=None): assert min != -1 or max != -1, 'At least one of `min` or `max` must be specified.' assert max == -1 or min <= max, '`min` cannot be more than `max`.' self.min = min self.max = max self.message = message def __call__(self, form, field): l = field.data and len(field.data) or 0 if l < self.min or self.max != -1 and l > self.max: message = self.message if message is None: if self.max == -1: message = field.ngettext('Field must be at least %(min)d character long.', 'Field must be at least %(min)d characters long.', self.min) elif self.min == -1: message = field.ngettext('Field cannot be longer than %(max)d character.', 'Field cannot be longer than %(max)d characters.', self.max) else: message = field.gettext('Field must be between %(min)d and %(max)d characters long.') raise ValidationError(message % dict(min=self.min, max=self.max, length=l)) class NumberRange(object): """ Validates that a number is of a minimum and/or maximum value, inclusive. This will work with any comparable number type, such as floats and decimals, not just integers. :param min: The minimum required value of the number. If not provided, minimum value will not be checked. :param max: The maximum value of the number. If not provided, maximum value will not be checked. :param message: Error message to raise in case of a validation error. Can be interpolated using `%(min)s` and `%(max)s` if desired. Useful defaults are provided depending on the existence of min and max. """ def __init__(self, min=None, max=None, message=None): self.min = min self.max = max self.message = message def __call__(self, form, field): data = field.data if data is None or (self.min is not None and data < self.min) or \ (self.max is not None and data > self.max): message = self.message if message is None: # we use %(min)s interpolation to support floats, None, and # Decimals without throwing a formatting exception. if self.max is None: message = field.gettext('Number must be at least %(min)s.') elif self.min is None: message = field.gettext('Number must be at most %(max)s.') else: message = field.gettext('Number must be between %(min)s and %(max)s.') raise ValidationError(message % dict(min=self.min, max=self.max)) class Optional(object): """ Allows empty input and stops the validation chain from continuing. If input is empty, also removes prior errors (such as processing errors) from the field. :param strip_whitespace: If True (the default) also stop the validation chain on input which consists of only whitespace. """ field_flags = ('optional', ) def __init__(self, strip_whitespace=True): if strip_whitespace: self.string_check = lambda s: s.strip() else: self.string_check = lambda s: s def __call__(self, form, field): if not field.raw_data or isinstance(field.raw_data[0], string_types) and not self.string_check(field.raw_data[0]): field.errors[:] = [] raise StopValidation() class DataRequired(object): """ Checks the field's data is 'truthy' otherwise stops the validation chain. This validator checks that the ``data`` attribute on the field is a 'true' value (effectively, it does ``if field.data``.) Furthermore, if the data is a string type, a string containing only whitespace characters is considered false. If the data is empty, also removes prior errors (such as processing errors) from the field. **NOTE** this validator used to be called `Required` but the way it behaved (requiring coerced data, not input data) meant it functioned in a way which was not symmetric to the `Optional` validator and furthermore caused confusion with certain fields which coerced data to 'falsey' values like ``0``, ``Decimal(0)``, ``time(0)`` etc. Unless a very specific reason exists, we recommend using the :class:`InputRequired` instead. :param message: Error message to raise in case of a validation error. """ field_flags = ('required', ) def __init__(self, message=None): self.message = message def __call__(self, form, field): if not field.data or isinstance(field.data, string_types) and not field.data.strip(): if self.message is None: message = field.gettext('This field is required.') else: message = self.message field.errors[:] = [] raise StopValidation(message) class Required(DataRequired): """ Legacy alias for DataRequired. This is needed over simple aliasing for those who require that the class-name of required be 'Required.' """ def __init__(self, *args, **kwargs): super(Required, self).__init__(*args, **kwargs) warnings.warn( 'Required is going away in WTForms 3.0, use DataRequired', DeprecationWarning, stacklevel=2 ) class InputRequired(object): """ Validates that input was provided for this field. Note there is a distinction between this and DataRequired in that InputRequired looks that form-input data was provided, and DataRequired looks at the post-coercion data. """ field_flags = ('required', ) def __init__(self, message=None): self.message = message def __call__(self, form, field): if not field.raw_data or not field.raw_data[0]: if self.message is None: message = field.gettext('This field is required.') else: message = self.message field.errors[:] = [] raise StopValidation(message) class Regexp(object): """ Validates the field against a user provided regexp. :param regex: The regular expression string to use. Can also be a compiled regular expression pattern. :param flags: The regexp flags to use, for example re.IGNORECASE. Ignored if `regex` is not a string. :param message: Error message to raise in case of a validation error. """ def __init__(self, regex, flags=0, message=None): if isinstance(regex, string_types): regex = re.compile(regex, flags) self.regex = regex self.message = message def __call__(self, form, field, message=None): match = self.regex.match(field.data or '') if not match: if message is None: if self.message is None: message = field.gettext('Invalid input.') else: message = self.message raise ValidationError(message) return match class Email(object): """ Validates an email address. Note that this uses a very primitive regular expression and should only be used in instances where you later verify by other means, such as email activation or lookups. :param message: Error message to raise in case of a validation error. """ user_regex = re.compile( r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*\Z" # dot-atom r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"\Z)', # quoted-string re.IGNORECASE) def __init__(self, message=None): self.message = message self.validate_hostname = HostnameValidation( require_tld=True, ) def __call__(self, form, field): value = field.data message = self.message if message is None: message = field.gettext('Invalid email address.') if not value or '@' not in value: raise ValidationError(message) user_part, domain_part = value.rsplit('@', 1) if not self.user_regex.match(user_part): raise ValidationError(message) if not self.validate_hostname(domain_part): raise ValidationError(message) class IPAddress(object): """ Validates an IP address. :param ipv4: If True, accept IPv4 addresses as valid (default True) :param ipv6: If True, accept IPv6 addresses as valid (default False) :param message: Error message to raise in case of a validation error. """ def __init__(self, ipv4=True, ipv6=False, message=None): if not ipv4 and not ipv6: raise ValueError('IP Address Validator must have at least one of ipv4 or ipv6 enabled.') self.ipv4 = ipv4 self.ipv6 = ipv6 self.message = message def __call__(self, form, field): value = field.data valid = False if value: valid = (self.ipv4 and self.check_ipv4(value)) or (self.ipv6 and self.check_ipv6(value)) if not valid: message = self.message if message is None: message = field.gettext('Invalid IP address.') raise ValidationError(message) @classmethod def check_ipv4(cls, value): parts = value.split('.') if len(parts) == 4 and all(x.isdigit() for x in parts): numbers = list(int(x) for x in parts) return all(num >= 0 and num < 256 for num in numbers) return False @classmethod def check_ipv6(cls, value): parts = value.split(':') if len(parts) > 8: return False num_blank = 0 for part in parts: if not part: num_blank += 1 else: try: value = int(part, 16) except ValueError: return False else: if value < 0 or value >= 65536: return False if num_blank < 2: return True elif num_blank == 2 and not parts[0] and not parts[1]: return True return False class MacAddress(Regexp): """ Validates a MAC address. :param message: Error message to raise in case of a validation error. """ def __init__(self, message=None): pattern = r'^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$' super(MacAddress, self).__init__(pattern, message=message) def __call__(self, form, field): message = self.message if message is None: message = field.gettext('Invalid Mac address.') super(MacAddress, self).__call__(form, field, message) class URL(Regexp): """ Simple regexp based url validation. Much like the email validator, you probably want to validate the url later by other means if the url must resolve. :param require_tld: If true, then the domain-name portion of the URL must contain a .tld suffix. Set this to false if you want to allow domains like `localhost`. :param message: Error message to raise in case of a validation error. """ def __init__(self, require_tld=True, message=None): regex = r'^[a-z]+://(?P[^/:]+)(?P:[0-9]+)?(?P\/.*)?$' super(URL, self).__init__(regex, re.IGNORECASE, message) self.validate_hostname = HostnameValidation( require_tld=require_tld, allow_ip=True, ) def __call__(self, form, field): message = self.message if message is None: message = field.gettext('Invalid URL.') match = super(URL, self).__call__(form, field, message) if not self.validate_hostname(match.group('host')): raise ValidationError(message) class UUID(object): """ Validates a UUID. :param message: Error message to raise in case of a validation error. """ def __init__(self, message=None): self.message = message def __call__(self, form, field): message = self.message if message is None: message = field.gettext('Invalid UUID.') try: uuid.UUID(field.data) except ValueError: raise ValidationError(message) class AnyOf(object): """ Compares the incoming data to a sequence of valid inputs. :param values: A sequence of valid inputs. :param message: Error message to raise in case of a validation error. `%(values)s` contains the list of values. :param values_formatter: Function used to format the list of values in the error message. """ def __init__(self, values, message=None, values_formatter=None): self.values = values self.message = message if values_formatter is None: values_formatter = self.default_values_formatter self.values_formatter = values_formatter def __call__(self, form, field): if field.data not in self.values: message = self.message if message is None: message = field.gettext('Invalid value, must be one of: %(values)s.') raise ValidationError(message % dict(values=self.values_formatter(self.values))) @staticmethod def default_values_formatter(values): return ', '.join(text_type(x) for x in values) class NoneOf(object): """ Compares the incoming data to a sequence of invalid inputs. :param values: A sequence of invalid inputs. :param message: Error message to raise in case of a validation error. `%(values)s` contains the list of values. :param values_formatter: Function used to format the list of values in the error message. """ def __init__(self, values, message=None, values_formatter=None): self.values = values self.message = message if values_formatter is None: values_formatter = self.default_values_formatter self.values_formatter = values_formatter def __call__(self, form, field): if field.data in self.values: message = self.message if message is None: message = field.gettext('Invalid value, can\'t be any of: %(values)s.') raise ValidationError(message % dict(values=self.values_formatter(self.values))) @staticmethod def default_values_formatter(v): return ', '.join(text_type(x) for x in v) class HostnameValidation(object): """ Helper class for checking hostnames for validation. This is not a validator in and of itself, and as such is not exported. """ hostname_part = re.compile(r'^(xn-|[a-z0-9]+)(-[a-z0-9]+)*$', re.IGNORECASE) tld_part = re.compile(r'^([a-z]{2,20}|xn--([a-z0-9]+-)*[a-z0-9]+)$', re.IGNORECASE) def __init__(self, require_tld=True, allow_ip=False): self.require_tld = require_tld self.allow_ip = allow_ip def __call__(self, hostname): if self.allow_ip: if IPAddress.check_ipv4(hostname) or IPAddress.check_ipv6(hostname): return True # Encode out IDNA hostnames. This makes further validation easier. try: hostname = hostname.encode('idna') except UnicodeError: pass # Turn back into a string in Python 3x if not isinstance(hostname, string_types): hostname = hostname.decode('ascii') if len(hostname) > 253: return False # Check that all labels in the hostname are valid parts = hostname.split('.') for part in parts: if not part or len(part) > 63: return False if not self.hostname_part.match(part): return False if self.require_tld: if len(parts) < 2 or not self.tld_part.match(parts[-1]): return False return True email = Email equal_to = EqualTo ip_address = IPAddress mac_address = MacAddress length = Length number_range = NumberRange optional = Optional required = Required input_required = InputRequired data_required = DataRequired regexp = Regexp url = URL any_of = AnyOf none_of = NoneOf