manage.get.gov/src/registrar/models/utility/domain_helper.py
2024-01-25 13:46:17 -07:00

186 lines
7.2 KiB
Python

import re
from typing import Type
from django.db import models
from django import forms
from django.http import JsonResponse
from api.views import DOMAIN_API_MESSAGES, check_domain_available
from registrar.utility import errors
from epplibwrapper.errors import RegistryError
from registrar.utility.enums import ValidationReturnType
class DomainHelper:
"""Utility functions and constants for domain names."""
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")
# a domain can be no longer than 253 characters in total
MAX_LENGTH = 253
@classmethod
def string_could_be_domain(cls, domain: str | None) -> bool:
"""Return True if the string could be a domain name, otherwise False."""
if not isinstance(domain, str):
return False
return bool(cls.DOMAIN_REGEX.match(domain))
@classmethod
def validate(cls, domain: str, blank_ok=False) -> str:
"""Attempt to determine if a domain name could be requested."""
# Split into pieces for the linter
domain = cls._validate_domain_string(domain, blank_ok)
try:
if not check_domain_available(domain):
raise errors.DomainUnavailableError()
except RegistryError as err:
raise errors.RegistrySystemError() from err
return domain
@staticmethod
def _validate_domain_string(domain, blank_ok):
"""Normalize the domain string, and check its content"""
if domain is None:
raise errors.BlankValueError()
if not isinstance(domain, str):
raise errors.InvalidDomainError()
domain = domain.lower().strip()
if domain == "" and not blank_ok:
raise errors.BlankValueError()
elif domain == "":
# If blank ok is true, just return the domain
return domain
if domain.endswith(".gov"):
domain = domain[:-4]
if "." in domain:
raise errors.ExtraDotsError()
if not DomainHelper.string_could_be_domain(domain + ".gov"):
raise errors.InvalidDomainError()
return domain
@classmethod
def validate_and_handle_errors(cls, domain, return_type, blank_ok=False):
"""
Validates a domain and returns an appropriate response based on the validation result.
This method uses the `validate` method to validate the domain. If validation fails, it catches the exception,
maps it to a corresponding error code, and returns a response based on the `return_type` parameter.
Args:
domain (str): The domain to validate.
return_type (ValidationReturnType): Determines the type of response (JSON or form validation error).
blank_ok (bool, optional): If True, blank input does not raise an exception. Defaults to False.
Returns:
tuple: The validated domain (or None if validation failed), and the response (success or error).
""" # noqa
# Map each exception to a corresponding error code
error_map = {
errors.BlankValueError: "required",
errors.ExtraDotsError: "extra_dots",
errors.DomainUnavailableError: "unavailable",
errors.RegistrySystemError: "error",
errors.InvalidDomainError: "invalid",
}
validated = None
response = None
try:
# Attempt to validate the domain
validated = cls.validate(domain, blank_ok)
# Get a list of each possible exception, and the code to return
except tuple(error_map.keys()) as error:
# If an error is caught, get its type
error_type = type(error)
# Generate the response based on the error code and return type
response = DomainHelper._return_form_error_or_json_response(return_type, code=error_map.get(error_type))
else:
# For form validation, we do not need to display the success message
if return_type != ValidationReturnType.FORM_VALIDATION_ERROR:
response = DomainHelper._return_form_error_or_json_response(return_type, code="success", available=True)
# Return the validated domain and the response (either error or success)
return (validated, response)
@staticmethod
def _return_form_error_or_json_response(return_type: ValidationReturnType, code, available=False):
"""
Returns an error response based on the `return_type`.
If `return_type` is `FORM_VALIDATION_ERROR`, raises a form validation error.
If `return_type` is `JSON_RESPONSE`, returns a JSON response with 'available', 'code', and 'message' fields.
If `return_type` is neither, raises a ValueError.
Args:
return_type (ValidationReturnType): The type of error response.
code (str): The error code for the error message.
available (bool, optional): Availability, only used for JSON responses. Defaults to False.
Returns:
A JSON response or a form validation error.
Raises:
ValueError: If `return_type` is neither `FORM_VALIDATION_ERROR` nor `JSON_RESPONSE`.
""" # noqa
match return_type:
case ValidationReturnType.FORM_VALIDATION_ERROR:
raise forms.ValidationError(DOMAIN_API_MESSAGES[code], code=code)
case ValidationReturnType.JSON_RESPONSE:
return JsonResponse({"available": available, "code": code, "message": DOMAIN_API_MESSAGES[code]})
case _:
raise ValueError("Invalid return type specified")
@classmethod
def sld(cls, domain: str):
"""
Get the second level domain. Example: `gsa.gov` -> `gsa`.
If no TLD is present, returns the original string.
"""
return domain.split(".")[0]
@classmethod
def tld(cls, domain: str):
"""Get the top level domain. Example: `gsa.gov` -> `gov`."""
parts = domain.rsplit(".")
return parts[-1] if len(parts) > 1 else ""
@staticmethod
def get_common_fields(model_1: Type[models.Model], model_2: Type[models.Model]):
"""
Returns a set of field names that two Django models have in common, excluding the 'id' field.
Args:
model_1 (Type[models.Model]): The first Django model class.
model_2 (Type[models.Model]): The second Django model class.
Returns:
Set[str]: A set of field names that both models share.
Example:
If model_1 has fields {"id", "name", "color"} and model_2 has fields {"id", "color"},
the function will return {"color"}.
"""
# Get a list of the existing fields on model_1 and model_2
model_1_fields = set(field.name for field in model_1._meta.get_fields() if field != "id")
model_2_fields = set(field.name for field in model_2._meta.get_fields() if field != "id")
# Get the fields that exist on both DomainApplication and DomainInformation
common_fields = model_1_fields & model_2_fields
return common_fields