feat: better reject null characters in forms (#7472)

* feat: subclass ModelMultipleChoiceField to reject nuls

* refactor: Use custom ModelMultipleChoiceField

* fix: handle value=None
This commit is contained in:
Jennifer Richards 2024-05-28 12:34:55 -03:00 committed by GitHub
parent 79f858b7d7
commit 08e953995a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 52 additions and 23 deletions

View file

@ -38,6 +38,7 @@ from ietf.mailtrigger.forms import CcSelectForm
from ietf.message.utils import infer_message from ietf.message.utils import infer_message
from ietf.name.models import BallotPositionName, DocTypeName from ietf.name.models import BallotPositionName, DocTypeName
from ietf.person.models import Person 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.mail import send_mail_text, send_mail_preformatted
from ietf.utils.decorators import require_api_key from ietf.utils.decorators import require_api_key
from ietf.utils.response import permission_denied from ietf.utils.response import permission_denied
@ -931,7 +932,7 @@ def approve_ballot(request, name):
class ApproveDownrefsForm(forms.Form): class ApproveDownrefsForm(forms.Form):
checkboxes = forms.ModelMultipleChoiceField( checkboxes = ModelMultipleChoiceField(
widget = forms.CheckboxSelectMultiple, widget = forms.CheckboxSelectMultiple,
queryset = RelatedDocument.objects.none(), ) queryset = RelatedDocument.objects.none(), )

View file

@ -52,6 +52,7 @@ from ietf.person.models import Person, Email
from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of 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.textupload import get_cleaned_text_file_content
from ietf.utils import log from ietf.utils import log
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.response import permission_denied from ietf.utils.response import permission_denied
from ietf.utils.timezone import datetime_today, DEADLINE_TZINFO from ietf.utils.timezone import datetime_today, DEADLINE_TZINFO
@ -390,9 +391,9 @@ def replaces(request, name):
)) ))
class SuggestedReplacesForm(forms.Form): class SuggestedReplacesForm(forms.Form):
replaces = forms.ModelMultipleChoiceField(queryset=Document.objects.all(), replaces = ModelMultipleChoiceField(queryset=Document.objects.all(),
label="Suggestions", required=False, widget=forms.CheckboxSelectMultiple, label="Suggestions", required=False, widget=forms.CheckboxSelectMultiple,
help_text="Select only the documents that are replaced by this document") 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) comment = forms.CharField(label="Optional comment", widget=forms.Textarea, required=False, strip=False)
def __init__(self, suggested, *args, **kwargs): def __init__(self, suggested, *args, **kwargs):
@ -1601,7 +1602,7 @@ class ChangeStreamStateForm(forms.Form):
new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State' ) new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State' )
weeks = forms.IntegerField(label='Expected weeks in state',required=False) 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) 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): def __init__(self, *args, **kwargs):
doc = kwargs.pop("doc") doc = kwargs.pop("doc")

View file

@ -52,7 +52,7 @@ from ietf.utils.text import strip_prefix, xslugify
from ietf.utils.textupload import get_cleaned_text_file_content from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.utils.mail import send_mail_message from ietf.utils.mail import send_mail_message
from ietf.mailtrigger.utils import gather_address_lists 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.http import is_ajax
from ietf.utils.response import permission_denied from ietf.utils.response import permission_denied
from ietf.utils.timezone import date_today, DEADLINE_TZINFO from ietf.utils.timezone import date_today, DEADLINE_TZINFO
@ -68,7 +68,7 @@ def clean_doc_revision(doc, rev):
return rev return rev
class RequestReviewForm(forms.ModelForm): 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" }) deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={ "autoclose": "1", "start-date": "+0d" })
class Meta: class Meta:

View file

@ -69,6 +69,7 @@ from ietf.name.models import DocTagName, DocTypeName, StreamName
from ietf.person.models import Person from ietf.person.models import Person
from ietf.person.utils import get_active_ads from ietf.person.utils import get_active_ads
from ietf.utils.draft_search import normalize_draftname from ietf.utils.draft_search import normalize_draftname
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.log import log 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.doc.utils_search import prepare_document_table, doc_type, doc_state, doc_type_name, AD_WORKLOAD
from ietf.ietfauth.utils import has_role from ietf.ietfauth.utils import has_role
@ -100,7 +101,7 @@ class SearchForm(forms.Form):
("ad", "AD"), ("-ad", "AD (desc)"), ), ("ad", "AD"), ("-ad", "AD (desc)"), ),
required=False, widget=forms.HiddenInput) 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): def __init__(self, *args, **kwargs):
super(SearchForm, self).__init__(*args, **kwargs) super(SearchForm, self).__init__(*args, **kwargs)

View file

@ -32,7 +32,7 @@ from ietf.group.models import Group
from ietf.person.models import Email from ietf.person.models import Email
from ietf.person.fields import SearchableEmailField from ietf.person.fields import SearchableEmailField
from ietf.doc.models import Document 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 ietf.utils.timezone import date_today, datetime_from_date, DEADLINE_TZINFO
from functools import reduce from functools import reduce
@ -200,7 +200,7 @@ class SearchLiaisonForm(forms.Form):
return results return results
class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField): class CustomModelMultipleChoiceField(ModelMultipleChoiceField):
'''If value is a QuerySet, return it as is (for use in widget.render)''' '''If value is a QuerySet, return it as is (for use in widget.render)'''
def prepare_value(self, value): def prepare_value(self, value):
if isinstance(value, QuerySetAny): if isinstance(value, QuerySetAny):
@ -215,12 +215,12 @@ class CustomModelMultipleChoiceField(forms.ModelMultipleChoiceField):
class LiaisonModelForm(forms.ModelForm): class LiaisonModelForm(forms.ModelForm):
'''Specify fields which require a custom widget or that are not part of the model. '''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["class"] = "select2-field"
from_groups.widget.attrs['data-minimum-input-length'] = 0 from_groups.widget.attrs['data-minimum-input-length'] = 0
from_contact = forms.EmailField() # type: Union[forms.EmailField, SearchableEmailField] from_contact = forms.EmailField() # type: Union[forms.EmailField, SearchableEmailField]
to_contacts = forms.CharField(label="Contacts", widget=forms.Textarea(attrs={'rows':'3', }), strip=False) 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["class"] = "select2-field"
to_groups.widget.attrs['data-minimum-input-length'] = 0 to_groups.widget.attrs['data-minimum-input-length'] = 0
deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True) deadline = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label='Deadline', required=True)

View file

@ -28,7 +28,13 @@ from ietf.meeting.helpers import is_interim_meeting_approved, get_next_agenda_na
from ietf.message.models import Message from ietf.message.models import Message
from ietf.name.models import TimeSlotTypeName, SessionPurposeName from ietf.name.models import TimeSlotTypeName, SessionPurposeName
from ietf.person.models import Person 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, from ietf.utils.validators import ( validate_file_size, validate_mime_type,
validate_file_extension, validate_no_html_frame) validate_file_extension, validate_no_html_frame)
@ -551,7 +557,7 @@ class SwapTimeslotsForm(forms.Form):
queryset=TimeSlot.objects.none(), # default to none, fill in when we have a meeting queryset=TimeSlot.objects.none(), # default to none, fill in when we have a meeting
widget=forms.TextInput, widget=forms.TextInput,
) )
rooms = forms.ModelMultipleChoiceField( rooms = ModelMultipleChoiceField(
required=True, required=True,
queryset=Room.objects.none(), # default to none, fill in when we have a meeting queryset=Room.objects.none(), # default to none, fill in when we have a meeting
widget=CsvModelPkInput, widget=CsvModelPkInput,
@ -617,7 +623,7 @@ class TimeSlotCreateForm(forms.Form):
) )
duration = TimeSlotDurationField() duration = TimeSlotDurationField()
show_location = forms.BooleanField(required=False, initial=True) show_location = forms.BooleanField(required=False, initial=True)
locations = forms.ModelMultipleChoiceField( locations = ModelMultipleChoiceField(
queryset=Room.objects.none(), queryset=Room.objects.none(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
) )

View file

@ -21,6 +21,7 @@ from ietf.nomcom.utils import (NOMINATION_RECEIPT_TEMPLATE, FEEDBACK_RECEIPT_TEM
from ietf.person.models import Email from ietf.person.models import Email
from ietf.person.fields import (SearchableEmailField, SearchableEmailsField, from ietf.person.fields import (SearchableEmailField, SearchableEmailsField,
SearchablePersonField, SearchablePersonsField ) SearchablePersonField, SearchablePersonsField )
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.mail import send_mail from ietf.utils.mail import send_mail
from ietf.mailtrigger.utils import gather_address_lists from ietf.mailtrigger.utils import gather_address_lists
@ -719,9 +720,9 @@ class MutableFeedbackForm(forms.ModelForm):
required= self.feedback_type.slug != 'comment', required= self.feedback_type.slug != 'comment',
help_text='Hold down "Control", or "Command" on a Mac, to select more than one.') help_text='Hold down "Control", or "Command" on a Mac, to select more than one.')
if self.feedback_type.slug == 'comment': if self.feedback_type.slug == 'comment':
self.fields['topic'] = forms.ModelMultipleChoiceField(queryset=self.nomcom.topic_set.all(), 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.', help_text='Hold down "Control" or "Command" on a Mac, to select more than one.',
required=False,) required=False,)
else: else:
self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True), label="Position") 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) 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 EditNomineeForm(forms.ModelForm):
class NominationResponseCommentForm(forms.Form): 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) 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): def label_from_instance(self, obj):
year = obj.year() year = obj.year()
return f'Volunteer for the {year}/{year+1} Nominating Committee' return f'Volunteer for the {year}/{year+1} Nominating Committee'

View file

@ -13,6 +13,7 @@ from ietf.meeting.forms import sessiondetailsformset_factory
from ietf.meeting.models import ResourceAssociation, Constraint from ietf.meeting.models import ResourceAssociation, Constraint
from ietf.person.fields import SearchablePersonsField from ietf.person.fields import SearchablePersonsField
from ietf.person.models import Person from ietf.person.models import Person
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.html import clean_text_field from ietf.utils.html import clean_text_field
from ietf.utils import log from ietf.utils import log
@ -57,7 +58,7 @@ class GroupSelectForm(forms.Form):
self.fields['group'].widget.choices = choices self.fields['group'].widget.choices = choices
class NameModelMultipleChoiceField(forms.ModelMultipleChoiceField): class NameModelMultipleChoiceField(ModelMultipleChoiceField):
def label_from_instance(self, name): def label_from_instance(self, name):
return name.desc return name.desc
@ -159,7 +160,7 @@ class SessionForm(forms.Form):
self.fields['resources'].widget = forms.MultipleHiddenInput() self.fields['resources'].widget = forms.MultipleHiddenInput()
self.fields['timeranges'].widget = forms.MultipleHiddenInput() self.fields['timeranges'].widget = forms.MultipleHiddenInput()
# and entirely replace bethere - no need to support searching if input is hidden # 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, widget=forms.MultipleHiddenInput, required=False,
queryset=Person.objects.all(), queryset=Person.objects.all(),
) )

View file

@ -39,6 +39,7 @@ from ietf.submit.parsers.plain_parser import PlainParser
from ietf.submit.parsers.xml_parser import XMLParser from ietf.submit.parsers.xml_parser import XMLParser
from ietf.utils import log from ietf.utils import log
from ietf.utils.draft import PlaintextDraft from ietf.utils.draft import PlaintextDraft
from ietf.utils.fields import ModelMultipleChoiceField
from ietf.utils.text import normalize_text from ietf.utils.text import normalize_text
from ietf.utils.timezone import date_today from ietf.utils.timezone import date_today
from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError from ietf.utils.xmldraft import InvalidXMLError, XMLDraft, XMLParseError
@ -793,7 +794,7 @@ class EditSubmissionForm(forms.ModelForm):
rev = forms.CharField(label='Revision', max_length=2, required=True) rev = forms.CharField(label='Revision', max_length=2, required=True)
document_date = forms.DateField(required=True) document_date = forms.DateField(required=True)
pages = forms.IntegerField(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) 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) note = forms.CharField(label=mark_safe('Comment to the Secretariat'), widget=forms.Textarea, required=False, strip=False)

View file

@ -14,7 +14,7 @@ from typing import Optional, Type # pyflakes:ignore
from django import forms from django import forms
from django.db import models # pyflakes:ignore 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.core.exceptions import ValidationError
from django.utils.dateparse import parse_duration from django.utils.dateparse import parse_duration
@ -353,3 +353,20 @@ class MissingOkImageField(models.ImageField):
super().update_dimension_fields(*args, **kwargs) super().update_dimension_fields(*args, **kwargs)
except FileNotFoundError: except FileNotFoundError:
pass # don't do anything if the file has gone missing 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)