1import ipaddress
2import math
3import re
4import uuid
5
6__all__ = (
7 "DataRequired",
8 "data_required",
9 "Email",
10 "email",
11 "EqualTo",
12 "equal_to",
13 "IPAddress",
14 "ip_address",
15 "InputRequired",
16 "input_required",
17 "Length",
18 "length",
19 "NumberRange",
20 "number_range",
21 "Optional",
22 "optional",
23 "Regexp",
24 "regexp",
25 "URL",
26 "url",
27 "AnyOf",
28 "any_of",
29 "NoneOf",
30 "none_of",
31 "MacAddress",
32 "mac_address",
33 "UUID",
34 "ValidationError",
35 "StopValidation",
36 "readonly",
37 "ReadOnly",
38 "disabled",
39 "Disabled",
40)
41
42
43class ValidationError(ValueError):
44 """
45 Raised when a validator fails to validate its input.
46 """
47
48 def __init__(self, message="", *args, **kwargs):
49 ValueError.__init__(self, message, *args, **kwargs)
50
51
52class StopValidation(Exception):
53 """
54 Causes the validation chain to stop.
55
56 If StopValidation is raised, no more validators in the validation chain are
57 called. If raised with a message, the message will be added to the errors
58 list.
59 """
60
61 def __init__(self, message="", *args, **kwargs):
62 Exception.__init__(self, message, *args, **kwargs)
63
64
65class EqualTo:
66 """
67 Compares the values of two fields.
68
69 :param fieldname:
70 The name of the other field to compare to.
71 :param message:
72 Error message to raise in case of a validation error. Can be
73 interpolated with `%(other_label)s` and `%(other_name)s` to provide a
74 more helpful error.
75 """
76
77 def __init__(self, fieldname, message=None):
78 self.fieldname = fieldname
79 self.message = message
80
81 def __call__(self, form, field):
82 try:
83 other = form[self.fieldname]
84 except KeyError as exc:
85 raise ValidationError(
86 field.gettext("Invalid field name '%s'.") % self.fieldname
87 ) from exc
88 if field.data == other.data:
89 return
90
91 d = {
92 "other_label": hasattr(other, "label")
93 and other.label.text
94 or self.fieldname,
95 "other_name": self.fieldname,
96 }
97 message = self.message
98 if message is None:
99 message = field.gettext("Field must be equal to %(other_name)s.")
100
101 raise ValidationError(message % d)
102
103
104class Length:
105 """
106 Validates the length of a string.
107
108 :param min:
109 The minimum required length of the string. If not provided, minimum
110 length will not be checked.
111 :param max:
112 The maximum length of the string. If not provided, maximum length
113 will not be checked.
114 :param message:
115 Error message to raise in case of a validation error. Can be
116 interpolated using `%(min)d` and `%(max)d` if desired. Useful defaults
117 are provided depending on the existence of min and max.
118
119 When supported, sets the `minlength` and `maxlength` attributes on widgets.
120 """
121
122 def __init__(self, min=-1, max=-1, message=None):
123 assert (
124 min != -1 or max != -1
125 ), "At least one of `min` or `max` must be specified."
126 assert max == -1 or min <= max, "`min` cannot be more than `max`."
127 self.min = min
128 self.max = max
129 self.message = message
130 self.field_flags = {}
131 if self.min != -1:
132 self.field_flags["minlength"] = self.min
133 if self.max != -1:
134 self.field_flags["maxlength"] = self.max
135
136 def __call__(self, form, field):
137 length = field.data and len(field.data) or 0
138 if length >= self.min and (self.max == -1 or length <= self.max):
139 return
140
141 if self.message is not None:
142 message = self.message
143
144 elif self.max == -1:
145 message = field.ngettext(
146 "Field must be at least %(min)d character long.",
147 "Field must be at least %(min)d characters long.",
148 self.min,
149 )
150 elif self.min == -1:
151 message = field.ngettext(
152 "Field cannot be longer than %(max)d character.",
153 "Field cannot be longer than %(max)d characters.",
154 self.max,
155 )
156 elif self.min == self.max:
157 message = field.ngettext(
158 "Field must be exactly %(max)d character long.",
159 "Field must be exactly %(max)d characters long.",
160 self.max,
161 )
162 else:
163 message = field.gettext(
164 "Field must be between %(min)d and %(max)d characters long."
165 )
166
167 raise ValidationError(message % dict(min=self.min, max=self.max, length=length))
168
169
170class NumberRange:
171 """
172 Validates that a number is of a minimum and/or maximum value, inclusive.
173 This will work with any comparable number type, such as floats and
174 decimals, not just integers.
175
176 :param min:
177 The minimum required value of the number. If not provided, minimum
178 value will not be checked.
179 :param max:
180 The maximum value of the number. If not provided, maximum value
181 will not be checked.
182 :param message:
183 Error message to raise in case of a validation error. Can be
184 interpolated using `%(min)s` and `%(max)s` if desired. Useful defaults
185 are provided depending on the existence of min and max.
186
187 When supported, sets the `min` and `max` attributes on widgets.
188 """
189
190 def __init__(self, min=None, max=None, message=None):
191 self.min = min
192 self.max = max
193 self.message = message
194 self.field_flags = {}
195 if self.min is not None:
196 self.field_flags["min"] = self.min
197 if self.max is not None:
198 self.field_flags["max"] = self.max
199
200 def __call__(self, form, field):
201 data = field.data
202 if (
203 data is not None
204 and not math.isnan(data)
205 and (self.min is None or data >= self.min)
206 and (self.max is None or data <= self.max)
207 ):
208 return
209
210 if self.message is not None:
211 message = self.message
212
213 # we use %(min)s interpolation to support floats, None, and
214 # Decimals without throwing a formatting exception.
215 elif self.max is None:
216 message = field.gettext("Number must be at least %(min)s.")
217
218 elif self.min is None:
219 message = field.gettext("Number must be at most %(max)s.")
220
221 else:
222 message = field.gettext("Number must be between %(min)s and %(max)s.")
223
224 raise ValidationError(message % dict(min=self.min, max=self.max))
225
226
227class Optional:
228 """
229 Allows empty input and stops the validation chain from continuing.
230
231 If input is empty, also removes prior errors (such as processing errors)
232 from the field.
233
234 :param strip_whitespace:
235 If True (the default) also stop the validation chain on input which
236 consists of only whitespace.
237
238 Sets the `optional` attribute on widgets.
239 """
240
241 def __init__(self, strip_whitespace=True):
242 if strip_whitespace:
243 self.string_check = lambda s: s.strip()
244 else:
245 self.string_check = lambda s: s
246
247 self.field_flags = {"optional": True}
248
249 def __call__(self, form, field):
250 if (
251 not field.raw_data
252 or isinstance(field.raw_data[0], str)
253 and not self.string_check(field.raw_data[0])
254 ):
255 field.errors[:] = []
256 raise StopValidation()
257
258
259class DataRequired:
260 """
261 Checks the field's data is 'truthy' otherwise stops the validation chain.
262
263 This validator checks that the ``data`` attribute on the field is a 'true'
264 value (effectively, it does ``if field.data``.) Furthermore, if the data
265 is a string type, a string containing only whitespace characters is
266 considered false.
267
268 If the data is empty, also removes prior errors (such as processing errors)
269 from the field.
270
271 **NOTE** this validator used to be called `Required` but the way it behaved
272 (requiring coerced data, not input data) meant it functioned in a way
273 which was not symmetric to the `Optional` validator and furthermore caused
274 confusion with certain fields which coerced data to 'falsey' values like
275 ``0``, ``Decimal(0)``, ``time(0)`` etc. Unless a very specific reason
276 exists, we recommend using the :class:`InputRequired` instead.
277
278 :param message:
279 Error message to raise in case of a validation error.
280
281 Sets the `required` attribute on widgets.
282 """
283
284 def __init__(self, message=None):
285 self.message = message
286 self.field_flags = {"required": True}
287
288 def __call__(self, form, field):
289 if field.data and (not isinstance(field.data, str) or field.data.strip()):
290 return
291
292 if self.message is None:
293 message = field.gettext("This field is required.")
294 else:
295 message = self.message
296
297 field.errors[:] = []
298 raise StopValidation(message)
299
300
301class InputRequired:
302 """
303 Validates that input was provided for this field.
304
305 Note there is a distinction between this and DataRequired in that
306 InputRequired looks that form-input data was provided, and DataRequired
307 looks at the post-coercion data. This means that this validator only checks
308 whether non-empty data was sent, not whether non-empty data was coerced
309 from that data. Initially populated data is not considered sent.
310
311 Sets the `required` attribute on widgets.
312 """
313
314 def __init__(self, message=None):
315 self.message = message
316 self.field_flags = {"required": True}
317
318 def __call__(self, form, field):
319 if field.raw_data and field.raw_data[0]:
320 return
321
322 if self.message is None:
323 message = field.gettext("This field is required.")
324 else:
325 message = self.message
326
327 field.errors[:] = []
328 raise StopValidation(message)
329
330
331class Regexp:
332 """
333 Validates the field against a user provided regexp.
334
335 :param regex:
336 The regular expression string to use. Can also be a compiled regular
337 expression pattern.
338 :param flags:
339 The regexp flags to use, for example re.IGNORECASE. Ignored if
340 `regex` is not a string.
341 :param message:
342 Error message to raise in case of a validation error.
343 """
344
345 def __init__(self, regex, flags=0, message=None):
346 if isinstance(regex, str):
347 regex = re.compile(regex, flags)
348 self.regex = regex
349 self.message = message
350
351 def __call__(self, form, field, message=None):
352 match = self.regex.match(field.data or "")
353 if match:
354 return match
355
356 if message is None:
357 if self.message is None:
358 message = field.gettext("Invalid input.")
359 else:
360 message = self.message
361
362 raise ValidationError(message)
363
364
365class Email:
366 """
367 Validates an email address. Requires email_validator package to be
368 installed. For ex: pip install wtforms[email].
369
370 :param message:
371 Error message to raise in case of a validation error.
372 :param granular_message:
373 Use validation failed message from email_validator library
374 (Default False).
375 :param check_deliverability:
376 Perform domain name resolution check (Default False).
377 :param allow_smtputf8:
378 Fail validation for addresses that would require SMTPUTF8
379 (Default True).
380 :param allow_empty_local:
381 Allow an empty local part (i.e. @example.com), e.g. for validating
382 Postfix aliases (Default False).
383 """
384
385 def __init__(
386 self,
387 message=None,
388 granular_message=False,
389 check_deliverability=False,
390 allow_smtputf8=True,
391 allow_empty_local=False,
392 ):
393 self.message = message
394 self.granular_message = granular_message
395 self.check_deliverability = check_deliverability
396 self.allow_smtputf8 = allow_smtputf8
397 self.allow_empty_local = allow_empty_local
398
399 def __call__(self, form, field):
400 try:
401 import email_validator
402 except ImportError as exc: # pragma: no cover
403 raise Exception(
404 "Install 'email_validator' for email validation support."
405 ) from exc
406
407 try:
408 if field.data is None:
409 raise email_validator.EmailNotValidError()
410 email_validator.validate_email(
411 field.data,
412 check_deliverability=self.check_deliverability,
413 allow_smtputf8=self.allow_smtputf8,
414 allow_empty_local=self.allow_empty_local,
415 )
416 except email_validator.EmailNotValidError as e:
417 message = self.message
418 if message is None:
419 if self.granular_message:
420 message = field.gettext(e)
421 else:
422 message = field.gettext("Invalid email address.")
423 raise ValidationError(message) from e
424
425
426class IPAddress:
427 """
428 Validates an IP address.
429
430 :param ipv4:
431 If True, accept IPv4 addresses as valid (default True)
432 :param ipv6:
433 If True, accept IPv6 addresses as valid (default False)
434 :param message:
435 Error message to raise in case of a validation error.
436 """
437
438 def __init__(self, ipv4=True, ipv6=False, message=None):
439 if not ipv4 and not ipv6:
440 raise ValueError(
441 "IP Address Validator must have at least one of ipv4 or ipv6 enabled."
442 )
443 self.ipv4 = ipv4
444 self.ipv6 = ipv6
445 self.message = message
446
447 def __call__(self, form, field):
448 value = field.data
449 valid = False
450 if value:
451 valid = (self.ipv4 and self.check_ipv4(value)) or (
452 self.ipv6 and self.check_ipv6(value)
453 )
454
455 if valid:
456 return
457
458 message = self.message
459 if message is None:
460 message = field.gettext("Invalid IP address.")
461 raise ValidationError(message)
462
463 @classmethod
464 def check_ipv4(cls, value):
465 try:
466 address = ipaddress.ip_address(value)
467 except ValueError:
468 return False
469
470 if not isinstance(address, ipaddress.IPv4Address):
471 return False
472
473 return True
474
475 @classmethod
476 def check_ipv6(cls, value):
477 try:
478 address = ipaddress.ip_address(value)
479 except ValueError:
480 return False
481
482 if not isinstance(address, ipaddress.IPv6Address):
483 return False
484
485 return True
486
487
488class MacAddress(Regexp):
489 """
490 Validates a MAC address.
491
492 :param message:
493 Error message to raise in case of a validation error.
494 """
495
496 def __init__(self, message=None):
497 pattern = r"^(?:[0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$"
498 super().__init__(pattern, message=message)
499
500 def __call__(self, form, field):
501 message = self.message
502 if message is None:
503 message = field.gettext("Invalid Mac address.")
504
505 super().__call__(form, field, message)
506
507
508class URL(Regexp):
509 """
510 Simple regexp based url validation. Much like the email validator, you
511 probably want to validate the url later by other means if the url must
512 resolve.
513
514 :param require_tld:
515 If true, then the domain-name portion of the URL must contain a .tld
516 suffix. Set this to false if you want to allow domains like
517 `localhost`.
518 :param allow_ip:
519 If false, then give ip as host will fail validation
520 :param message:
521 Error message to raise in case of a validation error.
522 """
523
524 def __init__(self, require_tld=True, allow_ip=True, message=None):
525 regex = (
526 r"^[a-z]+://"
527 r"(?P<host>[^\/\?:]+)"
528 r"(?P<port>:[0-9]+)?"
529 r"(?P<path>\/.*?)?"
530 r"(?P<query>\?.*)?$"
531 )
532 super().__init__(regex, re.IGNORECASE, message)
533 self.validate_hostname = HostnameValidation(
534 require_tld=require_tld, allow_ip=allow_ip
535 )
536
537 def __call__(self, form, field):
538 message = self.message
539 if message is None:
540 message = field.gettext("Invalid URL.")
541
542 match = super().__call__(form, field, message)
543 if not self.validate_hostname(match.group("host")):
544 raise ValidationError(message)
545
546
547class UUID:
548 """
549 Validates a UUID.
550
551 :param message:
552 Error message to raise in case of a validation error.
553 """
554
555 def __init__(self, message=None):
556 self.message = message
557
558 def __call__(self, form, field):
559 message = self.message
560 if message is None:
561 message = field.gettext("Invalid UUID.")
562 try:
563 uuid.UUID(field.data)
564 except ValueError as exc:
565 raise ValidationError(message) from exc
566
567
568class AnyOf:
569 """
570 Compares the incoming data to a sequence of valid inputs.
571
572 :param values:
573 A sequence of valid inputs.
574 :param message:
575 Error message to raise in case of a validation error. `%(values)s`
576 contains the list of values.
577 :param values_formatter:
578 Function used to format the list of values in the error message.
579 """
580
581 def __init__(self, values, message=None, values_formatter=None):
582 self.values = values
583 self.message = message
584 if values_formatter is None:
585 values_formatter = self.default_values_formatter
586 self.values_formatter = values_formatter
587
588 def __call__(self, form, field):
589 data = field.data if isinstance(field.data, list) else [field.data]
590 if any(d in self.values for d in data):
591 return
592
593 message = self.message
594 if message is None:
595 message = field.gettext("Invalid value, must be one of: %(values)s.")
596
597 raise ValidationError(message % dict(values=self.values_formatter(self.values)))
598
599 @staticmethod
600 def default_values_formatter(values):
601 return ", ".join(str(x) for x in values)
602
603
604class NoneOf:
605 """
606 Compares the incoming data to a sequence of invalid inputs.
607
608 :param values:
609 A sequence of invalid inputs.
610 :param message:
611 Error message to raise in case of a validation error. `%(values)s`
612 contains the list of values.
613 :param values_formatter:
614 Function used to format the list of values in the error message.
615 """
616
617 def __init__(self, values, message=None, values_formatter=None):
618 self.values = values
619 self.message = message
620 if values_formatter is None:
621 values_formatter = self.default_values_formatter
622 self.values_formatter = values_formatter
623
624 def __call__(self, form, field):
625 data = field.data if isinstance(field.data, list) else [field.data]
626 if not any(d in self.values for d in data):
627 return
628
629 message = self.message
630 if message is None:
631 message = field.gettext("Invalid value, can't be any of: %(values)s.")
632
633 raise ValidationError(message % dict(values=self.values_formatter(self.values)))
634
635 @staticmethod
636 def default_values_formatter(v):
637 return ", ".join(str(x) for x in v)
638
639
640class HostnameValidation:
641 """
642 Helper class for checking hostnames for validation.
643
644 This is not a validator in and of itself, and as such is not exported.
645 """
646
647 hostname_part = re.compile(r"^(xn-|[a-z0-9_]+)(-[a-z0-9_-]+)*$", re.IGNORECASE)
648 tld_part = re.compile(r"^([a-z]{2,20}|xn--([a-z0-9]+-)*[a-z0-9]+)$", re.IGNORECASE)
649
650 def __init__(self, require_tld=True, allow_ip=False):
651 self.require_tld = require_tld
652 self.allow_ip = allow_ip
653
654 def __call__(self, hostname):
655 if self.allow_ip and (
656 IPAddress.check_ipv4(hostname) or IPAddress.check_ipv6(hostname)
657 ):
658 return True
659
660 # Encode out IDNA hostnames. This makes further validation easier.
661 try:
662 hostname = hostname.encode("idna")
663 except UnicodeError:
664 pass
665
666 # Turn back into a string in Python 3x
667 if not isinstance(hostname, str):
668 hostname = hostname.decode("ascii")
669
670 if len(hostname) > 253:
671 return False
672
673 # Check that all labels in the hostname are valid
674 parts = hostname.split(".")
675 for part in parts:
676 if not part or len(part) > 63:
677 return False
678 if not self.hostname_part.match(part):
679 return False
680
681 if self.require_tld and (len(parts) < 2 or not self.tld_part.match(parts[-1])):
682 return False
683
684 return True
685
686
687class ReadOnly:
688 """
689 Set a field readonly.
690
691 Validation fails if the form data is different than the
692 field object data, or if unset, from the field default data.
693 """
694
695 def __init__(self):
696 self.field_flags = {"readonly": True}
697
698 def __call__(self, form, field):
699 if field.data != field.object_data:
700 raise ValidationError(field.gettext("This field cannot be edited."))
701
702
703class Disabled:
704 """
705 Set a field disabled.
706
707 Validation fails if the form data has any value.
708 """
709
710 def __init__(self):
711 self.field_flags = {"disabled": True}
712
713 def __call__(self, form, field):
714 if field.raw_data is not None:
715 raise ValidationError(
716 field.gettext("This field is disabled and cannot have a value.")
717 )
718
719
720email = Email
721equal_to = EqualTo
722ip_address = IPAddress
723mac_address = MacAddress
724length = Length
725number_range = NumberRange
726optional = Optional
727input_required = InputRequired
728data_required = DataRequired
729regexp = Regexp
730url = URL
731any_of = AnyOf
732none_of = NoneOf
733readonly = ReadOnly
734disabled = Disabled