123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732 |
- import ipaddress
- import math
- import re
- import uuid
- __all__ = (
- "DataRequired",
- "data_required",
- "Email",
- "email",
- "EqualTo",
- "equal_to",
- "IPAddress",
- "ip_address",
- "InputRequired",
- "input_required",
- "Length",
- "length",
- "NumberRange",
- "number_range",
- "Optional",
- "optional",
- "Regexp",
- "regexp",
- "URL",
- "url",
- "AnyOf",
- "any_of",
- "NoneOf",
- "none_of",
- "MacAddress",
- "mac_address",
- "UUID",
- "ValidationError",
- "StopValidation",
- "readonly",
- "ReadOnly",
- "disabled",
- "Disabled",
- )
- 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:
- """
- 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 as exc:
- raise ValidationError(
- field.gettext("Invalid field name '%s'.") % self.fieldname
- ) from exc
- if field.data == other.data:
- return
- 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:
- """
- 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.
- When supported, sets the `minlength` and `maxlength` attributes on widgets.
- """
- 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
- self.field_flags = {}
- if self.min != -1:
- self.field_flags["minlength"] = self.min
- if self.max != -1:
- self.field_flags["maxlength"] = self.max
- def __call__(self, form, field):
- length = field.data and len(field.data) or 0
- if length >= self.min and (self.max == -1 or length <= self.max):
- return
- if self.message is not None:
- message = self.message
- elif 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,
- )
- elif self.min == self.max:
- message = field.ngettext(
- "Field must be exactly %(max)d character long.",
- "Field must be exactly %(max)d characters long.",
- 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=length))
- class NumberRange:
- """
- 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.
- When supported, sets the `min` and `max` attributes on widgets.
- """
- def __init__(self, min=None, max=None, message=None):
- self.min = min
- self.max = max
- self.message = message
- self.field_flags = {}
- if self.min is not None:
- self.field_flags["min"] = self.min
- if self.max is not None:
- self.field_flags["max"] = self.max
- def __call__(self, form, field):
- data = field.data
- if (
- data is not None
- and not math.isnan(data)
- and (self.min is None or data >= self.min)
- and (self.max is None or data <= self.max)
- ):
- return
- if self.message is not None:
- message = self.message
- # we use %(min)s interpolation to support floats, None, and
- # Decimals without throwing a formatting exception.
- elif 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:
- """
- 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.
- Sets the `optional` attribute on widgets.
- """
- def __init__(self, strip_whitespace=True):
- if strip_whitespace:
- self.string_check = lambda s: s.strip()
- else:
- self.string_check = lambda s: s
- self.field_flags = {"optional": True}
- def __call__(self, form, field):
- if (
- not field.raw_data
- or isinstance(field.raw_data[0], str)
- and not self.string_check(field.raw_data[0])
- ):
- field.errors[:] = []
- raise StopValidation()
- class DataRequired:
- """
- 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.
- Sets the `required` attribute on widgets.
- """
- def __init__(self, message=None):
- self.message = message
- self.field_flags = {"required": True}
- def __call__(self, form, field):
- if field.data and (not isinstance(field.data, str) or field.data.strip()):
- return
- if self.message is None:
- message = field.gettext("This field is required.")
- else:
- message = self.message
- field.errors[:] = []
- raise StopValidation(message)
- class InputRequired:
- """
- 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. This means that this validator only checks
- whether non-empty data was sent, not whether non-empty data was coerced
- from that data. Initially populated data is not considered sent.
- Sets the `required` attribute on widgets.
- """
- def __init__(self, message=None):
- self.message = message
- self.field_flags = {"required": True}
- def __call__(self, form, field):
- if field.raw_data and field.raw_data[0]:
- return
- if self.message is None:
- message = field.gettext("This field is required.")
- else:
- message = self.message
- field.errors[:] = []
- raise StopValidation(message)
- class Regexp:
- """
- 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, str):
- 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 match:
- return match
- if message is None:
- if self.message is None:
- message = field.gettext("Invalid input.")
- else:
- message = self.message
- raise ValidationError(message)
- class Email:
- """
- Validates an email address. Requires email_validator package to be
- installed. For ex: pip install wtforms[email].
- :param message:
- Error message to raise in case of a validation error.
- :param granular_message:
- Use validation failed message from email_validator library
- (Default False).
- :param check_deliverability:
- Perform domain name resolution check (Default False).
- :param allow_smtputf8:
- Fail validation for addresses that would require SMTPUTF8
- (Default True).
- :param allow_empty_local:
- Allow an empty local part (i.e. @example.com), e.g. for validating
- Postfix aliases (Default False).
- """
- def __init__(
- self,
- message=None,
- granular_message=False,
- check_deliverability=False,
- allow_smtputf8=True,
- allow_empty_local=False,
- ):
- self.message = message
- self.granular_message = granular_message
- self.check_deliverability = check_deliverability
- self.allow_smtputf8 = allow_smtputf8
- self.allow_empty_local = allow_empty_local
- def __call__(self, form, field):
- try:
- import email_validator
- except ImportError as exc: # pragma: no cover
- raise Exception(
- "Install 'email_validator' for email validation support."
- ) from exc
- try:
- if field.data is None:
- raise email_validator.EmailNotValidError()
- email_validator.validate_email(
- field.data,
- check_deliverability=self.check_deliverability,
- allow_smtputf8=self.allow_smtputf8,
- allow_empty_local=self.allow_empty_local,
- )
- except email_validator.EmailNotValidError as e:
- message = self.message
- if message is None:
- if self.granular_message:
- message = field.gettext(e)
- else:
- message = field.gettext("Invalid email address.")
- raise ValidationError(message) from e
- class IPAddress:
- """
- 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 valid:
- return
- message = self.message
- if message is None:
- message = field.gettext("Invalid IP address.")
- raise ValidationError(message)
- @classmethod
- def check_ipv4(cls, value):
- try:
- address = ipaddress.ip_address(value)
- except ValueError:
- return False
- if not isinstance(address, ipaddress.IPv4Address):
- return False
- return True
- @classmethod
- def check_ipv6(cls, value):
- try:
- address = ipaddress.ip_address(value)
- except ValueError:
- return False
- if not isinstance(address, ipaddress.IPv6Address):
- return False
- return True
- 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().__init__(pattern, message=message)
- def __call__(self, form, field):
- message = self.message
- if message is None:
- message = field.gettext("Invalid Mac address.")
- super().__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 allow_ip:
- If false, then give ip as host will fail validation
- :param message:
- Error message to raise in case of a validation error.
- """
- def __init__(self, require_tld=True, allow_ip=True, message=None):
- regex = (
- r"^[a-z]+://"
- r"(?P<host>[^\/\?:]+)"
- r"(?P<port>:[0-9]+)?"
- r"(?P<path>\/.*?)?"
- r"(?P<query>\?.*)?$"
- )
- super().__init__(regex, re.IGNORECASE, message)
- self.validate_hostname = HostnameValidation(
- require_tld=require_tld, allow_ip=allow_ip
- )
- def __call__(self, form, field):
- message = self.message
- if message is None:
- message = field.gettext("Invalid URL.")
- match = super().__call__(form, field, message)
- if not self.validate_hostname(match.group("host")):
- raise ValidationError(message)
- class UUID:
- """
- 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 as exc:
- raise ValidationError(message) from exc
- class AnyOf:
- """
- 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 in self.values:
- return
- 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(str(x) for x in values)
- class NoneOf:
- """
- 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 not in self.values:
- return
- 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(str(x) for x in v)
- class HostnameValidation:
- """
- 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 and (
- 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, str):
- 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 and (len(parts) < 2 or not self.tld_part.match(parts[-1])):
- return False
- return True
- class ReadOnly:
- """
- Set a field readonly.
- Validation fails if the form data is different than the
- field object data, or if unset, from the field default data.
- """
- def __init__(self):
- self.field_flags = {"readonly": True}
- def __call__(self, form, field):
- if field.data != field.object_data:
- raise ValidationError(field.gettext("This field cannot be edited"))
- class Disabled:
- """
- Set a field disabled.
- Validation fails if the form data has any value.
- """
- def __init__(self):
- self.field_flags = {"disabled": True}
- def __call__(self, form, field):
- if field.raw_data is not None:
- raise ValidationError(
- field.gettext("This field is disabled and cannot have a value")
- )
- email = Email
- equal_to = EqualTo
- ip_address = IPAddress
- mac_address = MacAddress
- length = Length
- number_range = NumberRange
- optional = Optional
- input_required = InputRequired
- data_required = DataRequired
- regexp = Regexp
- url = URL
- any_of = AnyOf
- none_of = NoneOf
- readonly = ReadOnly
- disabled = Disabled
|