191 lines
5.1 KiB
Python
191 lines
5.1 KiB
Python
import re
|
|
import ipaddress
|
|
|
|
# Curated IANA timezone list for the dropdown. Validation accepts any entry from this set.
|
|
VALID_TIMEZONES = [
|
|
'UTC',
|
|
# Americas
|
|
'America/New_York',
|
|
'America/Detroit',
|
|
'America/Indiana/Indianapolis',
|
|
'America/Chicago',
|
|
'America/Denver',
|
|
'America/Phoenix',
|
|
'America/Los_Angeles',
|
|
'America/Anchorage',
|
|
'America/Adak',
|
|
'Pacific/Honolulu',
|
|
'America/Toronto',
|
|
'America/Vancouver',
|
|
'America/Winnipeg',
|
|
'America/Halifax',
|
|
'America/St_Johns',
|
|
'America/Mexico_City',
|
|
'America/Bogota',
|
|
'America/Lima',
|
|
'America/Santiago',
|
|
'America/Caracas',
|
|
'America/Sao_Paulo',
|
|
'America/Argentina/Buenos_Aires',
|
|
'America/Montevideo',
|
|
# Europe
|
|
'Europe/London',
|
|
'Europe/Dublin',
|
|
'Europe/Lisbon',
|
|
'Europe/Paris',
|
|
'Europe/Berlin',
|
|
'Europe/Amsterdam',
|
|
'Europe/Brussels',
|
|
'Europe/Madrid',
|
|
'Europe/Rome',
|
|
'Europe/Zurich',
|
|
'Europe/Vienna',
|
|
'Europe/Stockholm',
|
|
'Europe/Oslo',
|
|
'Europe/Copenhagen',
|
|
'Europe/Helsinki',
|
|
'Europe/Warsaw',
|
|
'Europe/Prague',
|
|
'Europe/Budapest',
|
|
'Europe/Bucharest',
|
|
'Europe/Athens',
|
|
'Europe/Istanbul',
|
|
'Europe/Moscow',
|
|
'Europe/Kyiv',
|
|
# Africa
|
|
'Africa/Casablanca',
|
|
'Africa/Lagos',
|
|
'Africa/Cairo',
|
|
'Africa/Nairobi',
|
|
'Africa/Johannesburg',
|
|
# Asia
|
|
'Asia/Dubai',
|
|
'Asia/Tbilisi',
|
|
'Asia/Tehran',
|
|
'Asia/Karachi',
|
|
'Asia/Kolkata',
|
|
'Asia/Colombo',
|
|
'Asia/Dhaka',
|
|
'Asia/Yangon',
|
|
'Asia/Bangkok',
|
|
'Asia/Ho_Chi_Minh',
|
|
'Asia/Singapore',
|
|
'Asia/Kuala_Lumpur',
|
|
'Asia/Jakarta',
|
|
'Asia/Shanghai',
|
|
'Asia/Hong_Kong',
|
|
'Asia/Taipei',
|
|
'Asia/Manila',
|
|
'Asia/Seoul',
|
|
'Asia/Tokyo',
|
|
'Asia/Yakutsk',
|
|
'Asia/Vladivostok',
|
|
# Australia / Pacific
|
|
'Australia/Perth',
|
|
'Australia/Darwin',
|
|
'Australia/Adelaide',
|
|
'Australia/Brisbane',
|
|
'Australia/Sydney',
|
|
'Australia/Melbourne',
|
|
'Australia/Hobart',
|
|
'Pacific/Auckland',
|
|
'Pacific/Fiji',
|
|
'Pacific/Guam',
|
|
'Pacific/Honolulu',
|
|
]
|
|
|
|
_TIMEZONE_SET = set(VALID_TIMEZONES)
|
|
|
|
|
|
def _strip(value, pattern, max_len):
|
|
return re.sub(pattern, '', str(value).strip())[:max_len]
|
|
|
|
|
|
def text(value, max_len=200):
|
|
"""General description: letters, digits, spaces, basic punctuation. No quotes/braces/brackets/slashes."""
|
|
return _strip(value, r'''["'{}\[\]\\/<>;`^~]''', max_len)
|
|
|
|
def name(value, max_len=64):
|
|
"""Label/name: letters, digits, spaces, hyphens, underscores, dots."""
|
|
return _strip(value, r'[^A-Za-z0-9 \-_.]', max_len)
|
|
|
|
def hostname(value, max_len=253):
|
|
"""Hostname or domain: letters, digits, hyphens, dots. Lowercased."""
|
|
return _strip(value.lower(), r'[^a-z0-9\-.]', max_len)
|
|
|
|
def ip(value, max_len=45):
|
|
"""IPv4 or IPv6 address. Returns '' if not a valid address."""
|
|
cleaned = _strip(value, r'[^0-9a-fA-F.:]', max_len)
|
|
try:
|
|
ipaddress.ip_address(cleaned)
|
|
return cleaned
|
|
except ValueError:
|
|
return ''
|
|
|
|
def ip_or_cidr(value, max_len=49):
|
|
"""IP address or CIDR subnet. Returns '' if not valid."""
|
|
cleaned = _strip(value, r'[^0-9a-fA-F.:/]', max_len)
|
|
try:
|
|
if '/' in cleaned:
|
|
ipaddress.ip_network(cleaned, strict=False)
|
|
else:
|
|
ipaddress.ip_address(cleaned)
|
|
return cleaned
|
|
except ValueError:
|
|
return ''
|
|
|
|
def mac(value, max_len=17):
|
|
"""MAC address: hex digits and colons."""
|
|
return _strip(value.upper(), r'[^0-9A-F:]', max_len)
|
|
|
|
def url(value, max_len=500):
|
|
"""URL: printable ASCII except quotes, braces, brackets, backslash, spaces."""
|
|
return _strip(value, r'''["'{}\[\]\\ ]''', max_len)
|
|
|
|
def interface_name(value, max_len=32):
|
|
"""Network interface name: letters, digits, hyphens, underscores, dots."""
|
|
return _strip(value, r'[^A-Za-z0-9\-_.]', max_len)
|
|
|
|
def port(value):
|
|
"""Port number string, validated 1-65535. Returns '' if invalid."""
|
|
digits = re.sub(r'[^0-9]', '', str(value))
|
|
try:
|
|
n = int(digits)
|
|
if 1 <= n <= 65535:
|
|
return str(n)
|
|
except (ValueError, TypeError):
|
|
pass
|
|
return ''
|
|
|
|
def time_24h(value, max_len=5):
|
|
"""24-hour time HH:MM: digits and colon only."""
|
|
return _strip(value, r'[^0-9:]', max_len)
|
|
|
|
def email(value, max_len=254):
|
|
"""Email address: letters, digits, @, dot, hyphen, underscore, plus. Lowercased."""
|
|
return _strip(value.lower(), r'[^a-z0-9@.\-_+]', max_len)
|
|
|
|
def timezone(value):
|
|
"""Timezone string: must be in VALID_TIMEZONES list. Returns '' if not found."""
|
|
return value if value in _TIMEZONE_SET else ''
|
|
|
|
_DOTTED_TO_PREFIX = {
|
|
'255.0.0.0': 8, '255.255.0.0': 16, '255.255.255.0': 24,
|
|
'255.255.255.128': 25, '255.255.255.192': 26,
|
|
'255.255.255.224': 27, '255.255.255.240': 28,
|
|
'255.255.255.248': 29, '255.255.255.252': 30,
|
|
}
|
|
|
|
def subnet_mask(value):
|
|
"""Subnet prefix length 1-30 (integer). Also accepts legacy dotted notation.
|
|
Returns int on success, None if invalid."""
|
|
s = str(value).strip()
|
|
if s in _DOTTED_TO_PREFIX:
|
|
return _DOTTED_TO_PREFIX[s]
|
|
try:
|
|
n = int(s)
|
|
if 1 <= n <= 30:
|
|
return n
|
|
except (ValueError, TypeError):
|
|
pass
|
|
return None
|