Skip to content

Commit

Permalink
feat: better reject null characters in forms (#7472)
Browse files Browse the repository at this point in the history
* feat: subclass ModelMultipleChoiceField to reject nuls

* refactor: Use custom ModelMultipleChoiceField

* fix: handle value=None
  • Loading branch information
jennifer-richards committed May 28, 2024
1 parent 79f858b commit 08e9539
Show file tree
Hide file tree
Showing 10 changed files with 52 additions and 23 deletions.
3 changes: 2 additions & 1 deletion ietf/doc/views_ballot.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from ietf.message.utils import infer_message
from ietf.name.models import BallotPositionName, DocTypeName
from ietf.person.models import Person
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.mail import send_mail_text, send_mail_preformatted
from ietf.utils.decorators import require_api_key
from ietf.utils.response import permission_denied
Expand Down Expand Up @@ -931,7 +932,7 @@ def approve_ballot(request, name):


class ApproveDownrefsForm(forms.Form):
checkboxes = forms.ModelMultipleChoiceField(
checkboxes = ModelMultipleChoiceField(
widget = forms.CheckboxSelectMultiple,
queryset = RelatedDocument.objects.none(), )

Expand Down
9 changes: 5 additions & 4 deletions ietf/doc/views_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.utils import log
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.response import permission_denied
from ietf.utils.timezone import datetime_today, DEADLINE_TZINFO

Expand Down Expand Up @@ -390,9 +391,9 @@ def replaces(request, name):
))

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

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

def __init__(self, *args, **kwargs):
doc = kwargs.pop("doc")
Expand Down
4 changes: 2 additions & 2 deletions ietf/doc/views_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.utils.mail import send_mail_message
from ietf.mailtrigger.utils import gather_address_lists
from ietf.utils.fields import MultiEmailField
from ietf.utils.fields import ModelMultipleChoiceField, MultiEmailField
from ietf.utils.http import is_ajax
from ietf.utils.response import permission_denied
from ietf.utils.timezone import date_today, DEADLINE_TZINFO
Expand All @@ -68,7 +68,7 @@ def clean_doc_revision(doc, rev):
return rev

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

class Meta:
Expand Down
3 changes: 2 additions & 1 deletion ietf/doc/views_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
from ietf.person.models import Person
from ietf.person.utils import get_active_ads
from ietf.utils.draft_search import normalize_draftname
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.log import log
from ietf.doc.utils_search import prepare_document_table, doc_type, doc_state, doc_type_name, AD_WORKLOAD
from ietf.ietfauth.utils import has_role
Expand Down Expand Up @@ -100,7 +101,7 @@ class SearchForm(forms.Form):
("ad", "AD"), ("-ad", "AD (desc)"), ),
required=False, widget=forms.HiddenInput)

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

def __init__(self, *args, **kwargs):
super(SearchForm, self).__init__(*args, **kwargs)
Expand Down
8 changes: 4 additions & 4 deletions ietf/liaisons/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from ietf.person.models import Email
from ietf.person.fields import SearchableEmailField
from ietf.doc.models import Document
from ietf.utils.fields import DatepickerDateField
from ietf.utils.fields import DatepickerDateField, ModelMultipleChoiceField
from ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO
from functools import reduce

Expand Down Expand Up @@ -200,7 +200,7 @@ def get_results(self):
return results


class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
class CustomModelMultipleChoiceField(ModelMultipleChoiceField):
'''If value is a QuerySet, return it as is (for use in widget.render)'''
def prepare_value(self, value):
if isinstance(value, QuerySetAny):
Expand All @@ -215,12 +215,12 @@ def prepare_value(self, value):
class LiaisonModelForm(forms.ModelForm):
'''Specify fields which require a custom widget or that are not part of the model.
'''
from_groups = forms.ModelMultipleChoiceField(queryset=Group.objects.all(),label='Groups',required=False)
from_groups = ModelMultipleChoiceField(queryset=Group.objects.all(),label='Groups',required=False)
from_groups.widget.attrs["class"] = "select2-field"
from_groups.widget.attrs['data-minimum-input-length'] = 0
from_contact = forms.EmailField() # type: Union[forms.EmailField, SearchableEmailField]
to_contacts = forms.CharField(label="Contacts", widget=forms.Textarea(attrs={'rows':'3', }), strip=False)
to_groups = forms.ModelMultipleChoiceField(queryset=Group.objects,label='Groups',required=False)
to_groups = ModelMultipleChoiceField(queryset=Group.objects,label='Groups',required=False)
to_groups.widget.attrs["class"] = "select2-field"
to_groups.widget.attrs['data-minimum-input-length'] = 0
deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True)
Expand Down
12 changes: 9 additions & 3 deletions ietf/meeting/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@
from ietf.message.models import Message
from ietf.name.models import TimeSlotTypeName, SessionPurposeName
from ietf.person.models import Person
from ietf.utils.fields import DatepickerDateField, DurationField, MultiEmailField, DatepickerSplitDateTimeWidget
from ietf.utils.fields import (
DatepickerDateField,
DatepickerSplitDateTimeWidget,
DurationField,
ModelMultipleChoiceField,
MultiEmailField,
)
from ietf.utils.validators import ( validate_file_size, validate_mime_type,
validate_file_extension, validate_no_html_frame)

Expand Down Expand Up @@ -551,7 +557,7 @@ class SwapTimeslotsForm(forms.Form):
queryset=TimeSlot.objects.none(), # default to none, fill in when we have a meeting
widget=forms.TextInput,
)
rooms = forms.ModelMultipleChoiceField(
rooms = ModelMultipleChoiceField(
required=True,
queryset=Room.objects.none(), # default to none, fill in when we have a meeting
widget=CsvModelPkInput,
Expand Down Expand Up @@ -617,7 +623,7 @@ class TimeSlotCreateForm(forms.Form):
)
duration = TimeSlotDurationField()
show_location = forms.BooleanField(required=False, initial=True)
locations = forms.ModelMultipleChoiceField(
locations = ModelMultipleChoiceField(
queryset=Room.objects.none(),
widget=forms.CheckboxSelectMultiple,
)
Expand Down
9 changes: 5 additions & 4 deletions ietf/nomcom/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from ietf.person.models import Email
from ietf.person.fields import (SearchableEmailField, SearchableEmailsField,
SearchablePersonField, SearchablePersonsField )
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists

Expand Down Expand Up @@ -719,9 +720,9 @@ def set_nomcom(self, nomcom, person, instances=None):
required= self.feedback_type.slug != 'comment',
help_text='Hold down "Control", or "Command" on a Mac, to select more than one.')
if self.feedback_type.slug == 'comment':
self.fields['topic'] = forms.ModelMultipleChoiceField(queryset=self.nomcom.topic_set.all(),
help_text='Hold down "Control" or "Command" on a Mac, to select more than one.',
required=False,)
self.fields['topic'] = ModelMultipleChoiceField(queryset=self.nomcom.topic_set.all(),
help_text='Hold down "Control" or "Command" on a Mac, to select more than one.',
required=False,)
else:
self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True), label="Position")
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)
Expand Down Expand Up @@ -847,7 +848,7 @@ class Meta:
class NominationResponseCommentForm(forms.Form):
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)

class NomcomVolunteerMultipleChoiceField(forms.ModelMultipleChoiceField):
class NomcomVolunteerMultipleChoiceField(ModelMultipleChoiceField):
def label_from_instance(self, obj):
year = obj.year()
return f'Volunteer for the {year}/{year+1} Nominating Committee'
Expand Down
5 changes: 3 additions & 2 deletions ietf/secr/sreq/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ietf.meeting.models import ResourceAssociation, Constraint
from ietf.person.fields import SearchablePersonsField
from ietf.person.models import Person
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.html import clean_text_field
from ietf.utils import log

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


class NameModelMultipleChoiceField(forms.ModelMultipleChoiceField):
class NameModelMultipleChoiceField(ModelMultipleChoiceField):
def label_from_instance(self, name):
return name.desc

Expand Down Expand Up @@ -159,7 +160,7 @@ def __init__(self, group, meeting, data=None, *args, **kwargs):
self.fields['resources'].widget = forms.MultipleHiddenInput()
self.fields['timeranges'].widget = forms.MultipleHiddenInput()
# and entirely replace bethere - no need to support searching if input is hidden
self.fields['bethere'] = forms.ModelMultipleChoiceField(
self.fields['bethere'] = ModelMultipleChoiceField(
widget=forms.MultipleHiddenInput, required=False,
queryset=Person.objects.all(),
)
Expand Down
3 changes: 2 additions & 1 deletion ietf/submit/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from ietf.submit.parsers.xml_parser import XMLParser
from ietf.utils import log
from ietf.utils.draft import PlaintextDraft
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.text import normalize_text
from ietf.utils.timezone import date_today
from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError
Expand Down Expand Up @@ -793,7 +794,7 @@ class EditSubmissionForm(forms.ModelForm):
rev = forms.CharField(label='Revision', max_length=2, required=True)
document_date = forms.DateField(required=True)
pages = forms.IntegerField(required=True)
formal_languages = forms.ModelMultipleChoiceField(queryset=FormalLanguageName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
formal_languages = ModelMultipleChoiceField(queryset=FormalLanguageName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
abstract = forms.CharField(widget=forms.Textarea, required=True, strip=False)

note = forms.CharField(label=mark_safe('Comment to the Secretariat'), widget=forms.Textarea, required=False, strip=False)
Expand Down
19 changes: 18 additions & 1 deletion ietf/utils/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from django import forms
from django.db import models # pyflakes:ignore
from django.core.validators import validate_email
from django.core.validators import ProhibitNullCharactersValidator, validate_email
from django.core.exceptions import ValidationError
from django.utils.dateparse import parse_duration

Expand Down Expand Up @@ -353,3 +353,20 @@ def update_dimension_fields(self, *args, **kwargs):
super().update_dimension_fields(*args, **kwargs)
except FileNotFoundError:
pass # don't do anything if the file has gone missing


class ModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""ModelMultipleChoiceField that rejects null characters cleanly"""
validate_no_nulls = ProhibitNullCharactersValidator()

def clean(self, value):
try:
for item in value:
self.validate_no_nulls(item)
except TypeError:
# A TypeError probably means value is not iterable, which most commonly comes up
# with None as a value. If it's something more exotic, we don't know how to test
# for null characters anyway. Either way, trust the superclass clean() method to
# handle it.
pass
return super().clean(value)

0 comments on commit 08e9539

Please sign in to comment.