Skip to content

Commit 77cbc2b

Browse files
authored
Fix bugs with organization form and slug input (#12306)
Found today that we introduced some bugs on the organization form, namely we aren't validating the user provided slug, only validating the user provided name rendered to a slug. This also ensures we're slugging for DNS safely, as this is just prepended to project URLs without much validation. Closes #12305
1 parent 0e766a4 commit 77cbc2b

File tree

3 files changed

+48
-18
lines changed

3 files changed

+48
-18
lines changed

readthedocs/organizations/forms.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,30 @@ def __init__(self, *args, **kwargs):
6060
)
6161
super().__init__(*args, **kwargs)
6262

63-
def clean_name(self):
64-
"""Raise exception on duplicate organization slug."""
65-
name = self.cleaned_data["name"]
63+
def clean_slug(self):
64+
slug_source = self.cleaned_data["slug"]
6665

6766
# Skip slug validation on already created organizations.
6867
if self.instance.pk:
69-
return name
70-
71-
potential_slug = slugify(name)
72-
if not potential_slug:
73-
raise forms.ValidationError(_("Invalid organization name: no slug generated"))
74-
if Organization.objects.filter(slug=potential_slug).exists():
68+
return slug_source
69+
70+
slug = slugify(slug_source, dns_safe=True)
71+
if not slug:
72+
# If the was not empty, but renders down to something empty, the
73+
# user gave an invalid slug. However, we can't suggest anything
74+
# useful because the slug is empty. This is an edge case for input
75+
# like `---`, so the error here doesn't need to be very specific.
76+
raise forms.ValidationError(_("Invalid slug, use more valid characters."))
77+
elif slug != slug_source:
78+
# There is a difference between the slug from the front end code, or
79+
# the user is trying to submit the form without our front end code.
7580
raise forms.ValidationError(
76-
_("Organization %(name)s already exists"),
77-
params={"name": name},
81+
_("Invalid slug, use suggested slug '%(slug)s' instead"),
82+
params={"slug": slug},
7883
)
79-
return name
84+
if Organization.objects.filter(slug=slug).exists():
85+
raise forms.ValidationError(_("Slug is already used by another organization"))
86+
return slug
8087

8188

8289
class OrganizationSignupFormBase(OrganizationForm):
@@ -106,9 +113,6 @@ class Meta:
106113
def __init__(self, *args, **kwargs):
107114
super().__init__(*args, **kwargs)
108115

109-
# `slug` is not required since its value is auto-generated from `name` if not provided
110-
self.fields["slug"].required = False
111-
112116
@staticmethod
113117
def _create_default_teams(organization):
114118
organization.teams.create(name="Admins", access=ADMIN_ACCESS)

readthedocs/organizations/tests/test_forms.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,15 +143,27 @@ def test_add_duplicate_invite_by_email(self):
143143

144144

145145
class OrganizationSignupTest(OrganizationTestCase):
146-
def test_create_organization_with_empy_slug(self):
146+
def test_create_organization_with_empty_slug(self):
147147
data = {
148148
"name": "往事",
149149
"email": "[email protected]",
150150
}
151151
form = forms.OrganizationSignupForm(data, user=self.user)
152152
self.assertFalse(form.is_valid())
153153
self.assertEqual(
154-
"Invalid organization name: no slug generated", form.errors["name"][0]
154+
"This field is required.", form.errors["slug"][0]
155+
)
156+
157+
def test_create_organization_with_invalid_unicode_slug(self):
158+
data = {
159+
"name": "往事",
160+
"email": "[email protected]",
161+
"slug": "-",
162+
}
163+
form = forms.OrganizationSignupForm(data, user=self.user)
164+
self.assertFalse(form.is_valid())
165+
self.assertEqual(
166+
"Invalid slug, use more valid characters.", form.errors["slug"][0]
155167
)
156168

157169
def test_create_organization_with_big_name(self):
@@ -165,17 +177,20 @@ def test_create_organization_with_big_name(self):
165177

166178
def test_create_organization_with_existent_slug(self):
167179
data = {
168-
"name": "mozilla",
180+
"name": "Fauxzilla",
169181
"email": "[email protected]",
182+
"slug": "mozilla",
170183
}
171184
form = forms.OrganizationSignupForm(data, user=self.user)
172185
# there is already an organization with the slug ``mozilla`` (lowercase)
173186
self.assertFalse(form.is_valid())
187+
self.assertEqual("Slug is already used by another organization", form.errors["slug"][0])
174188

175189
def test_create_organization_with_nonexistent_slug(self):
176190
data = {
177191
"name": "My New Organization",
178192
"email": "[email protected]",
193+
"slug": "my-new-organization",
179194
}
180195
form = forms.OrganizationSignupForm(data, user=self.user)
181196
self.assertTrue(form.is_valid())
@@ -191,3 +206,13 @@ def test_create_organization_with_invalid_slug(self):
191206
form = forms.OrganizationSignupForm(data, user=self.user)
192207
self.assertFalse(form.is_valid())
193208
self.assertIn("consisting of letters, numbers", form.errors["slug"][0])
209+
210+
def test_create_organization_with_dns_invalid_slug(self):
211+
data = {
212+
"name": "My Org",
213+
"email": "[email protected]",
214+
"slug": "-invalid_slug-",
215+
}
216+
form = forms.OrganizationSignupForm(data, user=self.user)
217+
self.assertFalse(form.is_valid())
218+
self.assertIn("Invalid slug, use suggested slug 'invalid-slug' instead", form.errors["slug"][0])

readthedocs/organizations/tests/test_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ def test_organization_signup(self):
361361
data = {
362362
"name": "Testing Organization",
363363
"email": "[email protected]",
364+
"slug": "testing-organization",
364365
}
365366
resp = self.client.post(reverse("organization_create"), data=data)
366367
self.assertEqual(Organization.objects.count(), 1)

0 commit comments

Comments
 (0)