Browse Source

Fixes #20498: Apply validation regex to URL custom fields

The validation_regex field was not being enforced for URL type custom
fields. This fix adds regex validation in two places:

1. to_form_field() - Applies regex validator to form fields (UI validation)
2. validate() - Applies regex check in model validation (API/programmatic)

Note: The original issue reported UI validation only, but this fix also
adds API validation for consistency with text field behavior and to
ensure data integrity across all entry points.
Jason Novinger 3 months ago
parent
commit
6747c82a1a
2 changed files with 38 additions and 0 deletions
  1. 16 0
      netbox/extras/models/customfields.py
  2. 22 0
      netbox/extras/tests/test_customfields.py

+ 16 - 0
netbox/extras/models/customfields.py

@@ -535,6 +535,15 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
         # URL
         # URL
         elif self.type == CustomFieldTypeChoices.TYPE_URL:
         elif self.type == CustomFieldTypeChoices.TYPE_URL:
             field = LaxURLField(assume_scheme='https', required=required, initial=initial)
             field = LaxURLField(assume_scheme='https', required=required, initial=initial)
+            if self.validation_regex:
+                field.validators = [
+                    RegexValidator(
+                        regex=self.validation_regex,
+                        message=mark_safe(_("Values must match this regex: <code>{regex}</code>").format(
+                            regex=escape(self.validation_regex)
+                        ))
+                    )
+                ]
 
 
         # JSON
         # JSON
         elif self.type == CustomFieldTypeChoices.TYPE_JSON:
         elif self.type == CustomFieldTypeChoices.TYPE_JSON:
@@ -684,6 +693,13 @@ class CustomField(CloningMixin, ExportTemplatesMixin, ChangeLoggedModel):
                 if self.validation_regex and not re.match(self.validation_regex, value):
                 if self.validation_regex and not re.match(self.validation_regex, value):
                     raise ValidationError(_("Value must match regex '{regex}'").format(regex=self.validation_regex))
                     raise ValidationError(_("Value must match regex '{regex}'").format(regex=self.validation_regex))
 
 
+            # Validate URL field
+            elif self.type == CustomFieldTypeChoices.TYPE_URL:
+                if type(value) is not str:
+                    raise ValidationError(_("Value must be a string."))
+                if self.validation_regex and not re.match(self.validation_regex, value):
+                    raise ValidationError(_("Value must match regex '{regex}'").format(regex=self.validation_regex))
+
             # Validate integer
             # Validate integer
             elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
             elif self.type == CustomFieldTypeChoices.TYPE_INTEGER:
                 if type(value) is not int:
                 if type(value) is not int:

+ 22 - 0
netbox/extras/tests/test_customfields.py

@@ -1300,6 +1300,28 @@ class CustomFieldAPITest(APITestCase):
         response = self.client.patch(url, data, format='json', **self.header)
         response = self.client.patch(url, data, format='json', **self.header)
         self.assertHttpStatus(response, status.HTTP_200_OK)
         self.assertHttpStatus(response, status.HTTP_200_OK)
 
 
+    def test_url_regex_validation(self):
+        """
+        Test that validation_regex is applied to URL custom fields (fixes #20498).
+        """
+        site2 = Site.objects.get(name='Site 2')
+        url = reverse('dcim-api:site-detail', kwargs={'pk': site2.pk})
+        self.add_permissions('dcim.change_site')
+
+        cf_url = CustomField.objects.get(name='url_field')
+        cf_url.validation_regex = r'^https://'  # Require HTTPS
+        cf_url.save()
+
+        # Test invalid URL (http instead of https)
+        data = {'custom_fields': {'url_field': 'http://example.com'}}
+        response = self.client.patch(url, data, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_400_BAD_REQUEST)
+
+        # Test valid URL (https)
+        data = {'custom_fields': {'url_field': 'https://example.com'}}
+        response = self.client.patch(url, data, format='json', **self.header)
+        self.assertHttpStatus(response, status.HTTP_200_OK)
+
     def test_uniqueness_validation(self):
     def test_uniqueness_validation(self):
         # Create a unique custom field
         # Create a unique custom field
         cf_text = CustomField.objects.get(name='text_field')
         cf_text = CustomField.objects.get(name='text_field')