Repository: carpentries/amy
Issue: Illegal characters in Host.domain

Putting `http://domain.com/` in `Host.domain` breaks the server, because we don't match for these characters in `urls.py` (only `\\w` regex).

I think we could make a validator that disallows `\\W` characters.


Changed files:
workshops/views.py, workshops/forms.py, workshops/test/test_host.py

diff --git a/workshops/forms.py b/workshops/forms.py
index 6ec1b223..2e6741df 100644
--- a/workshops/forms.py
+++ b/workshops/forms.py
@@ -1,4 +1,5 @@
 from django import forms
+from django.core.validators import RegexValidator
 from django.forms import HiddenInput, CheckboxSelectMultiple
 
 from crispy_forms.helper import FormHelper
@@ -9,7 +10,7 @@ from django_countries.fields import CountryField
 from selectable import forms as selectable
 
 from workshops.models import (
-    Award, Event, Lesson, Person, Task, KnowledgeDomain, Airport,
+    Award, Event, Lesson, Person, Task, KnowledgeDomain, Airport, Host,
 )
 from workshops import lookups
 
@@ -412,3 +413,19 @@ class PersonTaskForm(forms.ModelForm):
         model = Task
         fields = '__all__'
         widgets = {'person': HiddenInput}
+
+
+class HostForm(forms.ModelForm):
+    domain = forms.CharField(
+        max_length=Host._meta.get_field('domain').max_length,
+        validators=[
+            RegexValidator(
+                '[^\\w\\.-]+', inverse_match=True,
+                message='Please enter only the domain (such as "math.esu.edu")'
+                        ' without a leading "http://" or a trailing "/".')
+        ],
+    )
+
+    class Meta:
+        model = Host
+        fields = ['domain', 'fullname', 'country', 'notes']
diff --git a/workshops/test/test_host.py b/workshops/test/test_host.py
index 0279f387..46327b76 100644
--- a/workshops/test/test_host.py
+++ b/workshops/test/test_host.py
@@ -47,3 +47,23 @@ class TestHost(TestBase):
 
             with self.assertRaises(Host.DoesNotExist):
                 Host.objects.get(domain=host_domain)
+
+    def test_host_invalid_chars_in_domain(self):
+        """Ensure users can't put wrong characters in the host's domain field.
+
+        Invalid characters are any that match `[^\\w\\.-]+`, ie. domain is
+        allowed only to have alphabet-like chars, dot and dash.
+
+        The reason for only these chars lies in `workshops/urls.py`.  The regex
+        for the host_details URL has `[\\w\\.-]+` matching...
+        """
+        data = {
+            'domain': 'http://beta.com/',
+            'fullname': self.host_beta.fullname,
+            'country': self.host_beta.country,
+            'notes': self.host_beta.notes,
+        }
+        url = reverse('host_edit', args=[self.host_beta.domain])
+        rv = self.client.post(url, data=data)
+        # make sure we're not updating to good values
+        assert rv.status_code == 200
diff --git a/workshops/views.py b/workshops/views.py
index d54ab22a..178a87b8 100644
--- a/workshops/views.py
+++ b/workshops/views.py
@@ -47,7 +47,7 @@ from workshops.forms import (
     EventForm, TaskForm, TaskFullForm, bootstrap_helper,
     bootstrap_helper_with_add, BadgeAwardForm, PersonAwardForm,
     PersonPermissionsForm, bootstrap_helper_filter, PersonMergeForm,
-    PersonTaskForm,
+    PersonTaskForm, HostForm,
 )
 from workshops.util import (
     upload_person_task_csv,  verify_upload_person_task,
@@ -157,8 +157,6 @@ def dashboard(request):
 
 #------------------------------------------------------------
 
-HOST_FIELDS = ['domain', 'fullname', 'country', 'notes']
-
 
 @login_required
 def all_hosts(request):
@@ -186,13 +184,13 @@ def host_details(request, host_domain):
 
 class HostCreate(LoginRequiredMixin, CreateViewContext):
     model = Host
-    fields = HOST_FIELDS
+    form_class = HostForm
     template_name = 'workshops/generic_form.html'
 
 
 class HostUpdate(LoginRequiredMixin, UpdateViewContext):
     model = Host
-    fields = HOST_FIELDS
+    form_class = HostForm
     slug_field = 'domain'
     slug_url_kwarg = 'host_domain'
     template_name = 'workshops/generic_form.html'