blender-studio/subscriptions/validators.py

117 lines
4.1 KiB
Python

"""Custom validators for subscription-related forms."""
from typing import Set
import logging
import re
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
import tldextract
from stdnum.eu import vat
import stdnum.exceptions
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
with open(settings.BASE_DIR / 'subscriptions' / 'common_email_domains.txt') as f:
domains = set(_.strip() for _ in f.readlines())
common_email_domains = '|'.join(domains)
re_common_email_domain = re.compile(f'^({common_email_domains})$', re.I)
re_valid_invoice_reference = re.compile('^[-_ #0-9a-z]*$', re.I)
class VATINValidator:
"""A validator for VAT identification numbers.
Currently only supports European VIES VAT identification numbers.
See See https://en.wikipedia.org/wiki/VAT_identification_number
"""
messages = {
'country_code': _(
'VAT identification number must start with a valid 2-letter country code.'
),
'vatin': _('%(vatin)s is not a valid VAT identification number.'),
'vies': _('%(vatin)s is not a registered VAT identification number.'),
'unavailable': _('Unable to verify VAT identification number. Please try again later.'),
}
def __call__(self, value):
"""Validate the given value as VATIN."""
try:
vat_number = vat.validate(value)
except stdnum.exceptions.ValidationError:
raise ValidationError(
self.messages['vatin'], code='vat_number', params={'vatin': value}
)
country_code = vat.guess_country(vat_number)
if not country_code:
raise ValidationError(
self.messages['country_code'],
code='vat_number',
)
# Attempt to validate the number against VIES
is_valid = False
try:
vies_response = vat.check_vies(vat_number)
logger.debug('Got response from VIES: %s', vies_response)
is_valid = vies_response.valid
except Exception:
logger.exception('Unabled to verify the VAT identification number')
raise ValidationError(self.messages['unavailable'], code='vat_number')
if not is_valid:
raise ValidationError(self.messages['vies'], code='vat_number', params={'vatin': value})
def _is_valid_domain(domain: str) -> bool:
if len(domain) > 255:
return False
parts = domain.split('.')
allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(?<!-)$", re.IGNORECASE)
return len(parts) > 1 and all(allowed.match(x) for x in parts)
def _is_common_email_domain(value: str) -> str:
return re_common_email_domain.match(value)
def validate_email_domain(value: str) -> str:
"""Ensure the given value is not an invalid host name or a common email domain name."""
value = value.lower().strip()
if value[-1] == ".":
value = value[:-1] # strip exactly one dot from the right, if present
if not _is_valid_domain(value):
raise ValidationError('Must be a valid domain name')
if _is_common_email_domain(value):
raise ValidationError('Domains of common email providers are not allowed')
return value
def validate_invoice_reference(value: str) -> str:
"""Ensure the given value is an acceptible invoice reference (Order.external_reference)."""
if not re_valid_invoice_reference.match(value):
raise ValidationError(
'Only the following are allowed: '
'letters (A-Z, a-z), digits (0-9), -, _, # and blank space.'
)
return value
def extract_domains(value: str) -> Set[str]:
"""Return FQDN and all its subdomains, extracted from a given value."""
domain = tldextract.extract(value)
if not domain.registered_domain:
return {}
fqdn = domain.fqdn.lower()
sld = domain.registered_domain.lower()
result = {sld, fqdn}
subdomains = domain.subdomain.lower().split('.')
if len(subdomains) > 1:
prefix = ''
for sub in subdomains:
prefix += sub + '.'
result.add(fqdn.replace(prefix, ''))
return result