Skip to content

Commit 08e9539

Browse files
feat: better reject null characters in forms (ietf-tools#7472)
* feat: subclass ModelMultipleChoiceField to reject nuls * refactor: Use custom ModelMultipleChoiceField * fix: handle value=None
1 parent 79f858b commit 08e9539

10 files changed

Lines changed: 52 additions & 23 deletions

File tree

ietf/doc/views_ballot.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from ietf.message.utils import infer_message
3939
from ietf.name.models import BallotPositionName, DocTypeName
4040
from ietf.person.models import Person
41+
from ietf.utils.fields import ModelMultipleChoiceField
4142
from ietf.utils.mail import send_mail_text, send_mail_preformatted
4243
from ietf.utils.decorators import require_api_key
4344
from ietf.utils.response import permission_denied
@@ -931,7 +932,7 @@ def approve_ballot(request, name):
931932

932933

933934
class ApproveDownrefsForm(forms.Form):
934-
checkboxes = forms.ModelMultipleChoiceField(
935+
checkboxes = ModelMultipleChoiceField(
935936
widget = forms.CheckboxSelectMultiple,
936937
queryset = RelatedDocument.objects.none(), )
937938

ietf/doc/views_draft.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of
5353
from ietf.utils.textupload import get_cleaned_text_file_content
5454
from ietf.utils import log
55+
from ietf.utils.fields import ModelMultipleChoiceField
5556
from ietf.utils.response import permission_denied
5657
from ietf.utils.timezone import datetime_today, DEADLINE_TZINFO
5758

@@ -390,9 +391,9 @@ def replaces(request, name):
390391
))
391392

392393
class SuggestedReplacesForm(forms.Form):
393-
replaces = forms.ModelMultipleChoiceField(queryset=Document.objects.all(),
394-
label="Suggestions", required=False, widget=forms.CheckboxSelectMultiple,
395-
help_text="Select only the documents that are replaced by this document")
394+
replaces = ModelMultipleChoiceField(queryset=Document.objects.all(),
395+
label="Suggestions", required=False, widget=forms.CheckboxSelectMultiple,
396+
help_text="Select only the documents that are replaced by this document")
396397
comment = forms.CharField(label="Optional comment", widget=forms.Textarea, required=False, strip=False)
397398

398399
def __init__(self, suggested, *args, **kwargs):
@@ -1601,7 +1602,7 @@ class ChangeStreamStateForm(forms.Form):
16011602
new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State' )
16021603
weeks = forms.IntegerField(label='Expected weeks in state',required=False)
16031604
comment = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional comment for the document history.", strip=False)
1604-
tags = forms.ModelMultipleChoiceField(queryset=DocTagName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
1605+
tags = ModelMultipleChoiceField(queryset=DocTagName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
16051606

16061607
def __init__(self, *args, **kwargs):
16071608
doc = kwargs.pop("doc")

ietf/doc/views_review.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
from ietf.utils.textupload import get_cleaned_text_file_content
5353
from ietf.utils.mail import send_mail_message
5454
from ietf.mailtrigger.utils import gather_address_lists
55-
from ietf.utils.fields import MultiEmailField
55+
from ietf.utils.fields import ModelMultipleChoiceField, MultiEmailField
5656
from ietf.utils.http import is_ajax
5757
from ietf.utils.response import permission_denied
5858
from ietf.utils.timezone import date_today, DEADLINE_TZINFO
@@ -68,7 +68,7 @@ def clean_doc_revision(doc, rev):
6868
return rev
6969

7070
class RequestReviewForm(forms.ModelForm):
71-
team = forms.ModelMultipleChoiceField(queryset=Group.objects.all(), widget=forms.CheckboxSelectMultiple)
71+
team = ModelMultipleChoiceField(queryset=Group.objects.all(), widget=forms.CheckboxSelectMultiple)
7272
deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1", "start-date": "+0d" })
7373

7474
class Meta:

ietf/doc/views_search.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
from ietf.person.models import Person
7070
from ietf.person.utils import get_active_ads
7171
from ietf.utils.draft_search import normalize_draftname
72+
from ietf.utils.fields import ModelMultipleChoiceField
7273
from ietf.utils.log import log
7374
from ietf.doc.utils_search import prepare_document_table, doc_type, doc_state, doc_type_name, AD_WORKLOAD
7475
from ietf.ietfauth.utils import has_role
@@ -100,7 +101,7 @@ class SearchForm(forms.Form):
100101
("ad", "AD"), ("-ad", "AD (desc)"), ),
101102
required=False, widget=forms.HiddenInput)
102103

103-
doctypes = forms.ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug__in=('draft', 'rfc', 'bcp', 'std', 'fyi', 'liai-att')).order_by('name'), required=False)
104+
doctypes = ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug__in=('draft', 'rfc', 'bcp', 'std', 'fyi', 'liai-att')).order_by('name'), required=False)
104105

105106
def __init__(self, *args, **kwargs):
106107
super(SearchForm, self).__init__(*args, **kwargs)

ietf/liaisons/forms.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from ietf.person.models import Email
3333
from ietf.person.fields import SearchableEmailField
3434
from ietf.doc.models import Document
35-
from ietf.utils.fields import DatepickerDateField
35+
from ietf.utils.fields import DatepickerDateField, ModelMultipleChoiceField
3636
from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO
3737
from functools import reduce
3838

@@ -200,7 +200,7 @@ def get_results(self):
200200
return results
201201

202202

203-
class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
203+
class CustomModelMultipleChoiceField(ModelMultipleChoiceField):
204204
'''If value is a QuerySet, return it as is (for use in widget.render)'''
205205
def prepare_value(self, value):
206206
if isinstance(value, QuerySetAny):
@@ -215,12 +215,12 @@ def prepare_value(self, value):
215215
class LiaisonModelForm(forms.ModelForm):
216216
'''Specify fields which require a custom widget or that are not part of the model.
217217
'''
218-
from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label='Groups',required=False)
218+
from_groups = ModelMultipleChoiceField(queryset=Group.objects.all(),label='Groups',required=False)
219219
from_groups.widget.attrs["class"] = "select2-field"
220220
from_groups.widget.attrs['data-minimum-input-length'] = 0
221221
from_contact = forms.EmailField() # type: Union[forms.EmailField, SearchableEmailField]
222222
to_contacts = forms.CharField(label="Contacts", widget=forms.Textarea(attrs={'rows':'3', }), strip=False)
223-
to_groups = forms.ModelMultipleChoiceField(queryset=Group.objects,label='Groups',required=False)
223+
to_groups = ModelMultipleChoiceField(queryset=Group.objects,label='Groups',required=False)
224224
to_groups.widget.attrs["class"] = "select2-field"
225225
to_groups.widget.attrs['data-minimum-input-length'] = 0
226226
deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True)

ietf/meeting/forms.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@
2828
from ietf.message.models import Message
2929
from ietf.name.models import TimeSlotTypeName, SessionPurposeName
3030
from ietf.person.models import Person
31-
from ietf.utils.fields import DatepickerDateField, DurationField, MultiEmailField, DatepickerSplitDateTimeWidget
31+
from ietf.utils.fields import (
32+
DatepickerDateField,
33+
DatepickerSplitDateTimeWidget,
34+
DurationField,
35+
ModelMultipleChoiceField,
36+
MultiEmailField,
37+
)
3238
from ietf.utils.validators import ( validate_file_size, validate_mime_type,
3339
validate_file_extension, validate_no_html_frame)
3440

@@ -551,7 +557,7 @@ class SwapTimeslotsForm(forms.Form):
551557
queryset=TimeSlot.objects.none(), # default to none, fill in when we have a meeting
552558
widget=forms.TextInput,
553559
)
554-
rooms = forms.ModelMultipleChoiceField(
560+
rooms = ModelMultipleChoiceField(
555561
required=True,
556562
queryset=Room.objects.none(), # default to none, fill in when we have a meeting
557563
widget=CsvModelPkInput,
@@ -617,7 +623,7 @@ class TimeSlotCreateForm(forms.Form):
617623
)
618624
duration = TimeSlotDurationField()
619625
show_location = forms.BooleanField(required=False, initial=True)
620-
locations = forms.ModelMultipleChoiceField(
626+
locations = ModelMultipleChoiceField(
621627
queryset=Room.objects.none(),
622628
widget=forms.CheckboxSelectMultiple,
623629
)

ietf/nomcom/forms.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from ietf.person.models import Email
2222
from ietf.person.fields import (SearchableEmailField, SearchableEmailsField,
2323
SearchablePersonField, SearchablePersonsField )
24+
from ietf.utils.fields import ModelMultipleChoiceField
2425
from ietf.utils.mail import send_mail
2526
from ietf.mailtrigger.utils import gather_address_lists
2627

@@ -719,9 +720,9 @@ def set_nomcom(self, nomcom, person, instances=None):
719720
required= self.feedback_type.slug != 'comment',
720721
help_text='Hold down "Control", or "Command" on a Mac, to select more than one.')
721722
if self.feedback_type.slug == 'comment':
722-
self.fields['topic'] = forms.ModelMultipleChoiceField(queryset=self.nomcom.topic_set.all(),
723-
help_text='Hold down "Control" or "Command" on a Mac, to select more than one.',
724-
required=False,)
723+
self.fields['topic'] = ModelMultipleChoiceField(queryset=self.nomcom.topic_set.all(),
724+
help_text='Hold down "Control" or "Command" on a Mac, to select more than one.',
725+
required=False,)
725726
else:
726727
self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True), label="Position")
727728
self.fields['searched_email'] = SearchableEmailField(only_users=False,help_text="Try to find the candidate you are classifying with this field first. Only use the name and email fields below if this search does not find the candidate.",label="Candidate",required=False)
@@ -847,7 +848,7 @@ class Meta:
847848
class NominationResponseCommentForm(forms.Form):
848849
comments = forms.CharField(widget=forms.Textarea,required=False,help_text="Any comments provided will be encrypted and will only be visible to the NomCom.", strip=False)
849850

850-
class NomcomVolunteerMultipleChoiceField(forms.ModelMultipleChoiceField):
851+
class NomcomVolunteerMultipleChoiceField(ModelMultipleChoiceField):
851852
def label_from_instance(self, obj):
852853
year = obj.year()
853854
return f'Volunteer for the {year}/{year+1} Nominating Committee'

ietf/secr/sreq/forms.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ietf.meeting.models import ResourceAssociation, Constraint
1414
from ietf.person.fields import SearchablePersonsField
1515
from ietf.person.models import Person
16+
from ietf.utils.fields import ModelMultipleChoiceField
1617
from ietf.utils.html import clean_text_field
1718
from ietf.utils import log
1819

@@ -57,7 +58,7 @@ def __init__(self,*args,**kwargs):
5758
self.fields['group'].widget.choices = choices
5859

5960

60-
class NameModelMultipleChoiceField(forms.ModelMultipleChoiceField):
61+
class NameModelMultipleChoiceField(ModelMultipleChoiceField):
6162
def label_from_instance(self, name):
6263
return name.desc
6364

@@ -159,7 +160,7 @@ def __init__(self, group, meeting, data=None, *args, **kwargs):
159160
self.fields['resources'].widget = forms.MultipleHiddenInput()
160161
self.fields['timeranges'].widget = forms.MultipleHiddenInput()
161162
# and entirely replace bethere - no need to support searching if input is hidden
162-
self.fields['bethere'] = forms.ModelMultipleChoiceField(
163+
self.fields['bethere'] = ModelMultipleChoiceField(
163164
widget=forms.MultipleHiddenInput, required=False,
164165
queryset=Person.objects.all(),
165166
)

ietf/submit/forms.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from ietf.submit.parsers.xml_parser import XMLParser
4040
from ietf.utils import log
4141
from ietf.utils.draft import PlaintextDraft
42+
from ietf.utils.fields import ModelMultipleChoiceField
4243
from ietf.utils.text import normalize_text
4344
from ietf.utils.timezone import date_today
4445
from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError
@@ -793,7 +794,7 @@ class EditSubmissionForm(forms.ModelForm):
793794
rev = forms.CharField(label='Revision', max_length=2, required=True)
794795
document_date = forms.DateField(required=True)
795796
pages = forms.IntegerField(required=True)
796-
formal_languages = forms.ModelMultipleChoiceField(queryset=FormalLanguageName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
797+
formal_languages = ModelMultipleChoiceField(queryset=FormalLanguageName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
797798
abstract = forms.CharField(widget=forms.Textarea, required=True, strip=False)
798799

799800
note = forms.CharField(label=mark_safe('Comment to the Secretariat'), widget=forms.Textarea, required=False, strip=False)

ietf/utils/fields.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from django import forms
1616
from django.db import models # pyflakes:ignore
17-
from django.core.validators import validate_email
17+
from django.core.validators import ProhibitNullCharactersValidator, validate_email
1818
from django.core.exceptions import ValidationError
1919
from django.utils.dateparse import parse_duration
2020

@@ -353,3 +353,20 @@ def update_dimension_fields(self, *args, **kwargs):
353353
super().update_dimension_fields(*args, **kwargs)
354354
except FileNotFoundError:
355355
pass # don't do anything if the file has gone missing
356+
357+
358+
class ModelMultipleChoiceField(forms.ModelMultipleChoiceField):
359+
"""ModelMultipleChoiceField that rejects null characters cleanly"""
360+
validate_no_nulls = ProhibitNullCharactersValidator()
361+
362+
def clean(self, value):
363+
try:
364+
for item in value:
365+
self.validate_no_nulls(item)
366+
except TypeError:
367+
# A TypeError probably means value is not iterable, which most commonly comes up
368+
# with None as a value. If it's something more exotic, we don't know how to test
369+
# for null characters anyway. Either way, trust the superclass clean() method to
370+
# handle it.
371+
pass
372+
return super().clean(value)

0 commit comments

Comments
 (0)