Coverage for utilities/validators.py: 55%
40 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import decimal
2import re
3from urllib.parse import urlparse
5from django.core.exceptions import ValidationError
6from django.core.validators import BaseValidator, RegexValidator, URLValidator, _lazy_re_compile
7from django.utils.translation import gettext_lazy as _
9from netbox.config import get_config
11__all__ = (
12 'ColorValidator',
13 'EnhancedURLValidator',
14 'ExclusionValidator',
15 'MultipleOfValidator',
16 'url_scheme_is_allowed',
17 'validate_regex',
18)
21ColorValidator = RegexValidator(
22 regex='^[0-9a-f]{6}$',
23 message='Enter a valid hexadecimal RGB color code.',
24 code='invalid'
25)
28class EnhancedURLValidator(URLValidator):
29 """
30 Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension and enforce allowed
31 schemes specified in the configuration.
32 """
33 fqdn_re = URLValidator.hostname_re + URLValidator.domain_re + URLValidator.tld_re
34 host_res = [URLValidator.ipv4_re, URLValidator.ipv6_re, fqdn_re, URLValidator.hostname_re]
35 regex = _lazy_re_compile(
36 r'^(?:[a-z0-9\.\-\+]*)://' # Scheme (enforced separately)
37 r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?' # HTTP basic authentication
38 r'(?:' + '|'.join(host_res) + ')' # IPv4, IPv6, FQDN, or hostname
39 r'(?::\d{1,5})?' # Port number
40 r'(?:[/?#][^\s]*)?' # Path
41 r'\Z', re.IGNORECASE)
42 schemes = None
44 def __call__(self, value):
45 if self.schemes is None:
46 # We can't load the allowed schemes until the configuration has been initialized
47 self.schemes = get_config().ALLOWED_URL_SCHEMES
48 return super().__call__(value)
51class ExclusionValidator(BaseValidator):
52 """
53 Ensure that a field's value is not equal to any of the specified values.
54 """
55 message = 'This value may not be %(show_value)s.'
57 def compare(self, a, b):
58 return a in b
61class MultipleOfValidator(BaseValidator):
62 """
63 Checks that a field's value is a numeric multiple of the given value. Both values are
64 cast as Decimals for comparison.
65 """
66 def __init__(self, multiple):
67 self.multiple = decimal.Decimal(str(multiple))
68 super().__init__(limit_value=None)
70 def __call__(self, value):
71 if decimal.Decimal(str(value)) % self.multiple != 0:
72 raise ValidationError(
73 _("{value} must be a multiple of {multiple}.").format(value=value, multiple=self.multiple)
74 )
77def url_scheme_is_allowed(value):
78 """
79 Return True if the URL's scheme is permitted by ALLOWED_URL_SCHEMES. A schemeless (relative) value
80 is considered permitted.
82 The scheme is compared in lower case. A percent-encoded scheme (e.g. "javascript%3A…") yields no
83 scheme, matching browser behavior: a browser does not decode the scheme portion of an href, so such
84 a value is inert and is treated as relative. A malformed URL which cannot be parsed (e.g.
85 "http://[::1/foo") likewise yields no scheme.
86 """
87 try:
88 scheme = urlparse(value).scheme.lower()
89 except ValueError:
90 scheme = ''
91 return not scheme or scheme in get_config().ALLOWED_URL_SCHEMES
94def validate_regex(value):
95 """
96 Checks that the value is a valid regular expression. (Don't confuse this with RegexValidator, which *uses* a regex
97 to validate a value.)
98 """
99 try:
100 re.compile(value)
101 except re.error:
102 raise ValidationError(_("{value} is not a valid regular expression.").format(value=value))