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 description(value, max_len=200): """Human-readable description: letters, digits, hyphens, parentheses, commas, forward slashes, spaces. Whitespace collapsed; no sequential commas or slashes.""" s = re.sub(r'[^A-Za-z0-9\-()/,\s]', '', str(value)) s = re.sub(r'\s+', ' ', s) s = re.sub(r',{2,}', ',', s) s = re.sub(r'/{2,}', '/', s) s = re.sub(r'-{2,}', '-', s) s = re.sub(r'\({2,}', '(', s) s = re.sub(r'\){2,}', ')', s) return s.strip()[:max_len] def name(value, max_len=40): """Identifier: lowercase letters, digits, hyphens only. No sequential hyphens.""" s = re.sub(r'[\s_]+', '-', str(value).strip().lower()) s = re.sub(r'[^a-z0-9-]', '', s)[:max_len] s = re.sub(r'-{2,}', '-', s) return s.strip('-') def domainname(value, max_len=253): """Hostname or domain: letters, digits, hyphens, dots. Lowercased.""" return _strip(value.lower(), r'[^a-z0-9\-.]', max_len) def domainlist(lines): """Sanitize a list of domain name strings, returning only non-empty results.""" return [h for v in lines if (h := domainname(v))] 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): """MAC address in aa:bb:cc:dd:ee:ff format. Colons required; no other separators accepted. Returns lowercase colon-separated MAC if valid, '' otherwise.""" s = str(value).strip().lower() if re.fullmatch(r'([0-9a-f]{2}:){5}[0-9a-f]{2}', s): return s return '' 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 '' def filterlist(submitted, allowed): """Filter a list of submitted values to those present in the allowed set, after sanitizing each.""" allowed = set(allowed) return [n for v in submitted if (n := name(v)) in allowed] def filtervalue(value, allowed): """Return the sanitized value if it exists in the allowed set, otherwise ''.""" n = name(value) return n if n in set(allowed) 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