"""Forms for domain management.""" from django import forms from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator from django.forms import formset_factory from phonenumber_field.widgets import RegionalPhoneNumberWidget from registrar.utility.errors import ( NameserverError, NameserverErrorCodes as nsErrorCodes, DsDataError, DsDataErrorCodes, SecurityEmailError, SecurityEmailErrorCodes, ) from ..models import Contact, DomainInformation, Domain from .common import ( ALGORITHM_CHOICES, DIGEST_TYPE_CHOICES, ) import re class DomainAddUserForm(forms.Form): """Form for adding a user to a domain.""" email = forms.EmailField(label="Email") class DomainNameserverForm(forms.Form): """Form for changing nameservers.""" domain = forms.CharField(widget=forms.HiddenInput, required=False) server = forms.CharField(label="Name server", strip=True) ip = forms.CharField( label="IP address (IPv4 or IPv6)", strip=True, required=False, ) def __init__(self, *args, **kwargs): super(DomainNameserverForm, self).__init__(*args, **kwargs) # add custom error messages self.fields["server"].error_messages.update( { "required": "A minimum of 2 name servers are required.", } ) def clean(self): # clean is called from clean_forms, which is called from is_valid # after clean_fields. it is used to determine form level errors. # is_valid is typically called from view during a post cleaned_data = super().clean() self.clean_empty_strings(cleaned_data) server = cleaned_data.get("server", "") # remove ANY spaces in the server field server = server.replace(" ", "") # lowercase the server server = server.lower() cleaned_data["server"] = server ip = cleaned_data.get("ip", None) # remove ANY spaces in the ip field ip = ip.replace(" ", "") cleaned_data["ip"] = ip domain = cleaned_data.get("domain", "") ip_list = self.extract_ip_list(ip) # validate if the form has a server or an ip if (ip and ip_list) or server: self.validate_nameserver_ip_combo(domain, server, ip_list) return cleaned_data def clean_empty_strings(self, cleaned_data): ip = cleaned_data.get("ip", "") if ip and len(ip.strip()) == 0: cleaned_data["ip"] = None def extract_ip_list(self, ip): return [ip.strip() for ip in ip.split(",")] if ip else [] def validate_nameserver_ip_combo(self, domain, server, ip_list): try: Domain.checkHostIPCombo(domain, server, ip_list) except NameserverError as e: if e.code == nsErrorCodes.GLUE_RECORD_NOT_ALLOWED: self.add_error( "server", NameserverError( code=nsErrorCodes.GLUE_RECORD_NOT_ALLOWED, nameserver=domain, ip=ip_list, ), ) elif e.code == nsErrorCodes.MISSING_IP: self.add_error( "ip", NameserverError(code=nsErrorCodes.MISSING_IP, nameserver=domain, ip=ip_list), ) elif e.code == nsErrorCodes.MISSING_HOST: self.add_error( "server", NameserverError(code=nsErrorCodes.MISSING_HOST, nameserver=domain, ip=ip_list), ) elif e.code == nsErrorCodes.INVALID_HOST: self.add_error( "server", NameserverError(code=nsErrorCodes.INVALID_HOST, nameserver=server, ip=ip_list), ) else: self.add_error("ip", str(e)) class BaseNameserverFormset(forms.BaseFormSet): def clean(self): """ Check for duplicate entries in the formset. """ if any(self.errors): # Don't bother validating the formset unless each form is valid on its own return data = [] duplicates = [] for form in self.forms: if form.cleaned_data: value = form.cleaned_data["server"] if value in data: form.add_error( "server", NameserverError(code=nsErrorCodes.DUPLICATE_HOST, nameserver=value), ) duplicates.append(value) else: data.append(value) NameserverFormset = formset_factory( DomainNameserverForm, formset=BaseNameserverFormset, extra=1, max_num=13, validate_max=True, ) class ContactForm(forms.ModelForm): """Form for updating contacts.""" class Meta: model = Contact fields = ["first_name", "middle_name", "last_name", "title", "email", "phone"] widgets = { "first_name": forms.TextInput, "middle_name": forms.TextInput, "last_name": forms.TextInput, "title": forms.TextInput, "email": forms.EmailInput, "phone": RegionalPhoneNumberWidget, } # the database fields have blank=True so ModelForm doesn't create # required fields by default. Use this list in __init__ to mark each # of these fields as required required = ["first_name", "last_name", "title", "email", "phone"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # take off maxlength attribute for the phone number field # which interferes with out input_with_errors template tag self.fields["phone"].widget.attrs.pop("maxlength", None) for field_name in self.required: self.fields[field_name].required = True # Set custom form label self.fields["middle_name"].label = "Middle name (optional)" # Set custom error messages self.fields["first_name"].error_messages = {"required": "Enter your first name / given name."} self.fields["last_name"].error_messages = {"required": "Enter your last name / family name."} self.fields["title"].error_messages = { "required": "Enter your title or role in your organization (e.g., Chief Information Officer)" } self.fields["email"].error_messages = { "required": "Enter your email address in the required format, like name@example.com." } self.fields["phone"].error_messages["required"] = "Enter your phone number." class AuthorizingOfficialContactForm(ContactForm): """Form for updating authorizing official contacts.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Set custom error messages self.fields["first_name"].error_messages = { "required": "Enter the first name / given name of your authorizing official." } self.fields["last_name"].error_messages = { "required": "Enter the last name / family name of your authorizing official." } self.fields["title"].error_messages = { "required": "Enter the title or role your authorizing official has in your \ organization (e.g., Chief Information Officer)." } self.fields["email"].error_messages = { "required": "Enter an email address in the required format, like name@example.com." } self.fields["phone"].error_messages["required"] = "Enter a phone number for your authorizing official." class DomainSecurityEmailForm(forms.Form): """Form for adding or editing a security email to a domain.""" security_email = forms.EmailField( label="Security email (optional)", required=False, error_messages={ "invalid": str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)), }, ) class DomainOrgNameAddressForm(forms.ModelForm): """Form for updating the organization name and mailing address.""" zipcode = forms.CharField( label="Zip code", validators=[ RegexValidator( "^[0-9]{5}(?:-[0-9]{4})?$|^$", message="Enter a zip code in the form of 12345 or 12345-6789.", ) ], ) class Meta: model = DomainInformation fields = [ "federal_agency", "organization_name", "address_line1", "address_line2", "city", "state_territory", "zipcode", "urbanization", ] error_messages = { "federal_agency": {"required": "Select the federal agency for your organization."}, "organization_name": {"required": "Enter the name of your organization."}, "address_line1": {"required": "Enter the street address of your organization."}, "city": {"required": "Enter the city where your organization is located."}, "state_territory": { "required": "Select the state, territory, or military post where your organization is located." }, } widgets = { # We need to set the required attributed for federal_agency and # state/territory because for these fields we are creating an individual # instance of the Select. For the other fields we use the for loop to set # the class's required attribute to true. "federal_agency": forms.Select(attrs={"required": True}, choices=DomainInformation.AGENCY_CHOICES), "organization_name": forms.TextInput, "address_line1": forms.TextInput, "address_line2": forms.TextInput, "city": forms.TextInput, "state_territory": forms.Select( attrs={ "required": True, }, choices=DomainInformation.StateTerritoryChoices.choices, ), "urbanization": forms.TextInput, } # the database fields have blank=True so ModelForm doesn't create # required fields by default. Use this list in __init__ to mark each # of these fields as required required = ["organization_name", "address_line1", "city", "zipcode"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) for field_name in self.required: self.fields[field_name].required = True self.fields["state_territory"].widget.attrs.pop("maxlength", None) self.fields["zipcode"].widget.attrs.pop("maxlength", None) class DomainDnssecForm(forms.Form): """Form for enabling and disabling dnssec""" class DomainDsdataForm(forms.Form): """Form for adding or editing DNSSEC DS Data to a domain.""" def validate_hexadecimal(value): """ Tests that string matches all hexadecimal values. Raise validation error to display error in form if invalid characters entered """ if not re.match(r"^[0-9a-fA-F]+$", value): raise forms.ValidationError(str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS))) key_tag = forms.IntegerField( required=True, label="Key tag", validators=[ MinValueValidator(0, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))), MaxValueValidator(65535, message=str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE))), ], error_messages={"required": ("Key tag is required.")}, ) algorithm = forms.TypedChoiceField( required=True, label="Algorithm", coerce=int, # need to coerce into int so dsData objects can be compared choices=[(None, "--Select--")] + ALGORITHM_CHOICES, # type: ignore error_messages={"required": ("Algorithm is required.")}, ) digest_type = forms.TypedChoiceField( required=True, label="Digest type", coerce=int, # need to coerce into int so dsData objects can be compared choices=[(None, "--Select--")] + DIGEST_TYPE_CHOICES, # type: ignore error_messages={"required": ("Digest type is required.")}, ) digest = forms.CharField( required=True, label="Digest", validators=[validate_hexadecimal], error_messages={ "required": "Digest is required.", }, ) def clean(self): # clean is called from clean_forms, which is called from is_valid # after clean_fields. it is used to determine form level errors. # is_valid is typically called from view during a post cleaned_data = super().clean() digest_type = cleaned_data.get("digest_type", 0) digest = cleaned_data.get("digest", "") # validate length of digest depending on digest_type if digest_type == 1 and len(digest) != 40: self.add_error( "digest", DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1), ) elif digest_type == 2 and len(digest) != 64: self.add_error( "digest", DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA256), ) return cleaned_data DomainDsdataFormset = formset_factory( DomainDsdataForm, extra=0, can_delete=True, )