diff --git a/src/onegov/agency/forms/agency.py b/src/onegov/agency/forms/agency.py
index b80029ee0a..8dc860f930 100644
--- a/src/onegov/agency/forms/agency.py
+++ b/src/onegov/agency/forms/agency.py
@@ -16,7 +16,7 @@
from onegov.form.fields import ChosenSelectField, HtmlField
from onegov.form.fields import MultiCheckboxField
from onegov.form.fields import UploadField
-from onegov.form.validators import FileSizeLimit
+from onegov.form.validators import FileSizeLimit, MIME_TYPES_IMAGE
from onegov.form.validators import WhitelistedMimeType
from onegov.gis import CoordinatesField
from sqlalchemy import func
@@ -73,10 +73,7 @@ class ExtendedAgencyForm(Form):
organigram = UploadField(
label=_('Organigram'),
validators=[
- WhitelistedMimeType({
- 'image/jpeg',
- 'image/png',
- }),
+ WhitelistedMimeType(MIME_TYPES_IMAGE),
FileSizeLimit(1 * 1024 * 1024)
]
)
diff --git a/src/onegov/election_day/forms/election.py b/src/onegov/election_day/forms/election.py
index 2c720e4933..9d4ff54478 100644
--- a/src/onegov/election_day/forms/election.py
+++ b/src/onegov/election_day/forms/election.py
@@ -13,7 +13,7 @@
from onegov.form.fields import ChosenSelectMultipleField
from onegov.form.fields import PanelField
from onegov.form.fields import UploadField
-from onegov.form.validators import FileSizeLimit
+from onegov.form.validators import FileSizeLimit, MIME_TYPES_PDF
from onegov.form.validators import WhitelistedMimeType
from re import findall
from sqlalchemy import or_
@@ -323,7 +323,7 @@ class ElectionForm(Form):
explanations_pdf = UploadField(
label=_('Explanations (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
],
fieldset=_('Related link')
diff --git a/src/onegov/election_day/forms/election_compound.py b/src/onegov/election_day/forms/election_compound.py
index 61be3c5e4c..76db82f6e8 100644
--- a/src/onegov/election_day/forms/election_compound.py
+++ b/src/onegov/election_day/forms/election_compound.py
@@ -12,7 +12,7 @@
from onegov.form.fields import ChosenSelectMultipleField
from onegov.form.fields import PanelField
from onegov.form.fields import UploadField
-from onegov.form.validators import FileSizeLimit
+from onegov.form.validators import FileSizeLimit, MIME_TYPES_PDF
from onegov.form.validators import WhitelistedMimeType
from re import findall
from sqlalchemy import or_
@@ -228,7 +228,7 @@ class ElectionCompoundForm(Form):
explanations_pdf = UploadField(
label=_('Explanations (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
],
fieldset=_('Related link')
@@ -237,7 +237,7 @@ class ElectionCompoundForm(Form):
upper_apportionment_pdf = UploadField(
label=_('Upper apportionment (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
],
fieldset=_('Related link'),
@@ -247,7 +247,7 @@ class ElectionCompoundForm(Form):
lower_apportionment_pdf = UploadField(
label=_('Lower apportionment (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
],
fieldset=_('Related link'),
diff --git a/src/onegov/election_day/forms/upload/common.py b/src/onegov/election_day/forms/upload/common.py
index e50464640e..2a527194b8 100644
--- a/src/onegov/election_day/forms/upload/common.py
+++ b/src/onegov/election_day/forms/upload/common.py
@@ -11,8 +11,8 @@
'text/csv'
}
ALLOWED_MIME_TYPES_XML = {
- 'application/xml',
- 'text/xml',
+ 'application/xml', # official, standard
+ 'text/xml', # deprecated MIME type for XML content
'text/plain'
}
diff --git a/src/onegov/election_day/forms/vote.py b/src/onegov/election_day/forms/vote.py
index 1719e07baa..1b5df4a516 100644
--- a/src/onegov/election_day/forms/vote.py
+++ b/src/onegov/election_day/forms/vote.py
@@ -10,7 +10,7 @@
from onegov.form.fields import ChosenSelectField
from onegov.form.fields import PanelField
from onegov.form.fields import UploadField
-from onegov.form.validators import FileSizeLimit
+from onegov.form.validators import FileSizeLimit, MIME_TYPES_PDF
from onegov.form.validators import WhitelistedMimeType
from wtforms.fields import BooleanField
from wtforms.fields import DateField
@@ -280,7 +280,7 @@ class VoteForm(Form):
explanations_pdf = UploadField(
label=_('Explanations (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
],
fieldset=_('Related link')
diff --git a/src/onegov/file/utils.py b/src/onegov/file/utils.py
index f9daa6a386..75a8ce1549 100644
--- a/src/onegov/file/utils.py
+++ b/src/onegov/file/utils.py
@@ -74,15 +74,8 @@ def get_supported_image_mime_types() -> set[str]:
# Not all PIL formats register a mime type, fill in the blanks ourselves.
supported_types = {
- 'image/bmp',
- 'image/x-bmp',
'image/x-MS-bmp',
- 'image/x-icon',
- 'image/x-ico',
- 'image/x-win-bitmap',
- 'image/x-pcx',
- 'image/x-portable-pixmap',
- 'image/x-tga'
+ 'image/x-xcf',
}
for mime in Image.MIME.values():
diff --git a/src/onegov/form/fields.py b/src/onegov/form/fields.py
index fc5d621d3c..1fe93275df 100644
--- a/src/onegov/form/fields.py
+++ b/src/onegov/form/fields.py
@@ -18,7 +18,7 @@
from onegov.file.utils import IMAGE_MIME_TYPES_AND_SVG
from onegov.form import log, _
from onegov.form.utils import path_to_filename
-from onegov.form.validators import ValidPhoneNumber
+from onegov.form.validators import ValidPhoneNumber, WhitelistedMimeType
from onegov.form.widgets import ChosenSelectWidget
from onegov.form.widgets import LinkPanelWidget
from onegov.form.widgets import DurationInput
@@ -59,7 +59,7 @@
if TYPE_CHECKING:
from collections.abc import Callable, Iterable, Iterator, Sequence
from datetime import datetime
- from onegov.core.types import FileDict as StrictFileDict
+ from onegov.core.types import FileDict as StrictFileDict, LaxFileDict
from onegov.file import File
from onegov.form import Form
from onegov.form.types import (
@@ -274,28 +274,58 @@ class UploadField(FileField):
file: IO[bytes] | None
filename: str | None
- if TYPE_CHECKING:
- def __init__(
- self,
- label: str | None = None,
- validators: Validators[FormT, Self] | None = None,
- filters: Sequence[Filter] = (),
- description: str = '',
- id: str | None = None,
- default: Sequence[StrictFileDict] = (),
- widget: Widget[Self] | None = None,
- render_kw: dict[str, Any] | None = None,
- name: str | None = None,
- _form: BaseForm | None = None,
- _prefix: str = '',
- _translations: _SupportsGettextAndNgettext | None = None,
- _meta: DefaultMeta | None = None,
- # onegov specific kwargs that get popped off
- *,
- fieldset: str | None = None,
- depends_on: Sequence[Any] | None = None,
- pricing: PricingRules | None = None,
- ): ...
+ def __init__(
+ self,
+ label: str | None = None,
+ validators: Validators[FormT, Self] | None = None,
+ filters: Sequence[Filter] = (),
+ description: str = '',
+ id: str | None = None,
+ default: LaxFileDict | None = None,
+ widget: Widget[Self] | None = None,
+ render_kw: dict[str, Any] | None = None,
+ name: str | None = None,
+ allowed_mimetypes: Sequence[str] | None = None,
+ _form: BaseForm | None = None,
+ _prefix: str = '',
+ _translations: _SupportsGettextAndNgettext | None = None,
+ _meta: DefaultMeta | None = None,
+ # onegov specific kwargs that get popped off
+ *,
+ fieldset: str | None = None,
+ depends_on: Sequence[Any] | None = None,
+ pricing: PricingRules | None = None,
+ ):
+ validator = (
+ WhitelistedMimeType(allowed_mimetypes)
+ if allowed_mimetypes
+ else WhitelistedMimeType()
+ )
+
+ if validators:
+ validators = list(validators)
+ if not any(isinstance(validator, WhitelistedMimeType)
+ for validator in validators
+ ):
+ validators.append(validator)
+ else:
+ validators = [validator]
+
+ super().__init__(
+ label=label,
+ validators=validators,
+ filters=filters,
+ description=description,
+ id=id,
+ default=default,
+ widget=widget,
+ render_kw=render_kw,
+ name=name,
+ _form=_form,
+ _prefix=_prefix,
+ _translations=_translations,
+ _meta=_meta,
+ )
# this is not quite accurate, since it is either a dictionary with all
# the keys or none of the keys, which would make type narrowing easier
@@ -474,6 +504,7 @@ def __init__(
render_kw: dict[str, Any] | None = None,
name: str | None = None,
upload_widget: Widget[UploadField] | None = None,
+ allowed_mimetypes: Sequence[str] | None = None,
_form: BaseForm | None = None,
_prefix: str = '',
_translations: _SupportsGettextAndNgettext | None = None,
@@ -492,11 +523,11 @@ def __init__(
# a lot of the arguments we just pass through to the subfield
unbound_field = self.upload_field_class(
- validators=validators, # type:ignore[arg-type]
filters=filters,
description=description,
widget=upload_widget,
render_kw=render_kw,
+ allowed_mimetypes=allowed_mimetypes,
**extra_arguments
)
super().__init__(
@@ -507,6 +538,7 @@ def __init__(
id=id,
default=default,
widget=widget, # type:ignore[arg-type]
+ validators=[*(validators or ())],
render_kw=render_kw,
name=name,
_form=_form,
diff --git a/src/onegov/form/validators.py b/src/onegov/form/validators.py
index 7b0ffd3873..2dc3bcba11 100644
--- a/src/onegov/form/validators.py
+++ b/src/onegov/form/validators.py
@@ -12,6 +12,8 @@
from decimal import Decimal
from dateutil.relativedelta import relativedelta
from mimetypes import types_map
+
+from onegov.file.utils import get_supported_image_mime_types
from onegov.form import _
from onegov.form.errors import (
DuplicateLabelError,
@@ -36,7 +38,8 @@
from wtforms.validators import ValidationError
-from typing import Generic, TYPE_CHECKING
+from typing import Generic, TYPE_CHECKING, Any
+
if TYPE_CHECKING:
from collections.abc import Collection, Sequence
from onegov.core.orm import Base
@@ -119,13 +122,96 @@ def __call__(self, form: Form, field: Field) -> None:
if not field.data:
return
- if field.data.get('size', 0) > self.max_bytes:
+ if isinstance(field.data, list): # UploadMultipleField
+ for data in field.data:
+ if not data:
+ continue # in case of file deletion
+
+ self.validate_filesize(field, data)
+
+ else:
+ self.validate_filesize(field, field.data)
+
+ def validate_filesize(self, field: Field, data: dict[Any, Any]) -> None:
+ if data.get('size', 0) > self.max_bytes:
message = field.gettext(self.message).format(
humanize.naturalsize(self.max_bytes)
)
raise ValidationError(message)
+MIME_TYPES_PDF = {
+ 'application/pdf',
+}
+
+# for now not allowed by default
+MIME_TYPES_JSON = {
+ 'application/json',
+}
+
+MIME_TYPES_DOCUMENT = {
+ 'application/msword', # doc
+ 'application/rtf',
+ *MIME_TYPES_PDF,
+ 'application/excel',
+ 'application/vnd.ms-excel', # xls
+ ('application/vnd.openxmlformats-officedocument.'
+ 'presentationml.presentation'), # pptx
+ ('application/vnd.openxmlformats-officedocument.'
+ 'spreadsheetml.sheet'), # xlsx
+ ('application/vnd.openxmlformats-officedocument.'
+ 'wordprocessingml.document'), # docx
+ 'application/vnd.ms-office',
+ 'application/CDFV2', # old ms office docs
+ 'application/x-ole-storage', # old ms office docs
+ 'application/CDFV2-unknown' # old ms office docs
+}
+
+MIME_TYPES_EXCEL = {
+ 'application/excel',
+ 'application/vnd.ms-excel',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-office',
+ 'application/octet-stream',
+ 'application/x-ole-storage',
+}
+
+MIME_TYPES_XML = {
+ 'application/xml',
+}
+
+MIME_TYPES_ARCHIVE = {
+ 'application/zip',
+}
+
+MIME_TYPES_TEXT_DATA = {
+ 'text/csv',
+ 'text/plain',
+}
+
+MIME_TYPES_IMAGE = {
+ # allowed types based on PIL
+ *get_supported_image_mime_types(),
+ 'image/svg+xml',
+}
+
+MIME_TYPES_AUDIO = {
+ 'audio/mp4',
+ 'audio/mpeg',
+ 'audio/wav',
+ 'audio/webm', # weba
+}
+
+MIME_TYPES_VIDEO = {
+ 'video/mp4',
+ 'video/mpeg', # mpg, mpeg
+ 'video/ogg',
+ 'video/quicktime', # mov
+ 'video/webm', # webm
+ 'video/x-msvideo', # avi
+}
+
+
class WhitelistedMimeType:
""" Makes sure an uploaded file is in a whitelist of allowed mimetypes.
@@ -134,17 +220,13 @@ class WhitelistedMimeType:
"""
whitelist: Collection[str] = {
- 'application/excel',
- 'application/vnd.ms-excel',
- 'application/msword',
- 'application/pdf',
- 'application/zip',
- 'image/gif',
- 'image/jpeg',
- 'image/png',
- 'image/x-ms-bmp',
- 'text/plain',
- 'text/csv'
+ *MIME_TYPES_DOCUMENT,
+ *MIME_TYPES_XML,
+ *MIME_TYPES_ARCHIVE,
+ *MIME_TYPES_TEXT_DATA,
+ *MIME_TYPES_IMAGE,
+ *MIME_TYPES_AUDIO,
+ *MIME_TYPES_VIDEO,
}
message = _('Files of this type are not supported.')
@@ -157,8 +239,20 @@ def __call__(self, form: Form, field: Field) -> None:
if not field.data:
return
- if field.data['mimetype'] not in self.whitelist:
- raise ValidationError(field.gettext(self.message))
+ if isinstance(field.data, list): # UploadMultipleField
+ for data in field.data:
+ if not data:
+ continue # in case of file deletion
+
+ self.validate_mimetype(field, data)
+
+ else:
+ self.validate_mimetype(field, field.data)
+
+ def validate_mimetype(self, field: Field, data: dict[Any, Any]) -> None:
+ if data['mimetype'] not in self.whitelist:
+ message = field.gettext(self.message)
+ raise ValidationError(field.gettext(message))
class ExpectedExtensions(WhitelistedMimeType):
diff --git a/src/onegov/form/widgets.py b/src/onegov/form/widgets.py
index 42dffb1b6b..772844849f 100644
--- a/src/onegov/form/widgets.py
+++ b/src/onegov/form/widgets.py
@@ -305,7 +305,10 @@ def __call__(
""")
if force_simple or len(field) == 0:
- return simple_template.format(input_html)
+ return simple_template.format(input_html) + Markup('\n').join(
+ Markup('{}').format(error)
+ for error in field.errors
+ )
else:
existing_html = Markup('').join(
subfield(
diff --git a/src/onegov/landsgemeinde/forms/agenda.py b/src/onegov/landsgemeinde/forms/agenda.py
index e663d99d88..07f590355c 100644
--- a/src/onegov/landsgemeinde/forms/agenda.py
+++ b/src/onegov/landsgemeinde/forms/agenda.py
@@ -19,8 +19,12 @@
from onegov.form.fields import TimeField
from onegov.form.fields import UploadField
from onegov.form.forms import NamedFileForm
-from onegov.form.validators import FileSizeLimit
-from onegov.form.validators import WhitelistedMimeType
+from onegov.form.validators import (
+ FileSizeLimit,
+ MIME_TYPES_PDF,
+ MIME_TYPES_ARCHIVE,
+ WhitelistedMimeType
+)
from onegov.landsgemeinde import _
from onegov.landsgemeinde.layouts import DefaultLayout
from onegov.landsgemeinde.models import AgendaItem, LandsgemeindeFile
@@ -80,7 +84,7 @@ class AgendaItemForm(NamedFileForm):
label=_('Excerpt from the Memorial (PDF)'),
fieldset=_('Memorial'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
]
)
@@ -226,7 +230,7 @@ class AgendaItemUploadForm(Form):
label=_('Agenda Item ZIP'),
fieldset=_('Import'),
validators=[
- WhitelistedMimeType({'application/zip'}),
+ WhitelistedMimeType(MIME_TYPES_ARCHIVE),
FileSizeLimit(100 * 1024 * 1024)
]
)
diff --git a/src/onegov/landsgemeinde/forms/assembly.py b/src/onegov/landsgemeinde/forms/assembly.py
index ca4375e0fe..e69affc5bc 100644
--- a/src/onegov/landsgemeinde/forms/assembly.py
+++ b/src/onegov/landsgemeinde/forms/assembly.py
@@ -7,8 +7,13 @@
from onegov.form.fields import TimeField
from onegov.form.fields import UploadField
from onegov.form.forms import NamedFileForm
-from onegov.form.validators import FileSizeLimit
-from onegov.form.validators import WhitelistedMimeType
+from onegov.form.validators import (
+ FileSizeLimit,
+ MIME_TYPES_PDF,
+ MIME_TYPES_AUDIO,
+ MIME_TYPES_ARCHIVE,
+ WhitelistedMimeType
+)
from onegov.landsgemeinde import _
from onegov.landsgemeinde.layouts import DefaultLayout
from onegov.landsgemeinde.models import Assembly, LandsgemeindeFile
@@ -83,7 +88,7 @@ class AssemblyForm(NamedFileForm):
label=_('Memorial part 1 (PDF)'),
fieldset=_('Downloads'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
]
)
@@ -92,7 +97,7 @@ class AssemblyForm(NamedFileForm):
label=_('Memorial part 2 (PDF)'),
fieldset=_('Downloads'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
]
)
@@ -101,7 +106,7 @@ class AssemblyForm(NamedFileForm):
label=_('Supplement to the memorial (PDF)'),
fieldset=_('Downloads'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
]
)
@@ -110,7 +115,7 @@ class AssemblyForm(NamedFileForm):
label=_('Protocol (PDF)'),
fieldset=_('Downloads'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024)
]
)
@@ -119,7 +124,7 @@ class AssemblyForm(NamedFileForm):
label=_('Audio (MP3)'),
fieldset=_('Downloads'),
validators=[
- WhitelistedMimeType({'audio/mpeg'}),
+ WhitelistedMimeType(MIME_TYPES_AUDIO),
FileSizeLimit(600 * 1024 * 1024)
]
)
@@ -128,7 +133,7 @@ class AssemblyForm(NamedFileForm):
label=_('Memorial as audio for the visually impaired and blind'),
fieldset=_('Downloads'),
validators=[
- WhitelistedMimeType({'application/zip'}),
+ WhitelistedMimeType(MIME_TYPES_ARCHIVE),
FileSizeLimit(600 * 1024 * 1024)
]
)
diff --git a/src/onegov/org/assets/js/locale.js b/src/onegov/org/assets/js/locale.js
index 5771369e55..3a6226d9f0 100644
--- a/src/onegov/org/assets/js/locale.js
+++ b/src/onegov/org/assets/js/locale.js
@@ -30,7 +30,9 @@ var locales = {
"The server responded with an error. We have been informed and will investigate the problem.": "Auf dem Server ist ein Fehler aufgetreten. Wir wurden informiert und werden das Problem analysieren.",
"The server could not be reached. Please try again.": "Der Server konnte nicht erreicht werden. Bitte probieren Sie es noch einmal.",
"The site could not be found.": "Die Seite wurde nicht gefunden.",
- "Access denied. Please log in before continuing.": "Zugriff verweigert. Bitte melden Sie sich an bevor Sie weiterfahren."
+ "Access denied. Please log in before continuing.": "Zugriff verweigert. Bitte melden Sie sich an bevor Sie weiterfahren.",
+ "Unsupported Media Type": "Nicht unterstützter Medientyp",
+ "Upload failed": "Upload fehlgeschlagen"
},
fr: {
"Allocation": "Allocation",
@@ -63,7 +65,9 @@ var locales = {
"The server responded with an error. We have been informed and will investigate the problem.": "Le serveur a répondu par une erreur. Nous en avons été informés et nous étudierons le problème.",
"The server could not be reached. Please try again.": "Le serveur n'a pas pu être joint. Veuillez réessayer.",
"The site could not be found.": "Impossible de trouver le site.",
- "Access denied. Please log in before continuing.": "Accès refusé. Veuillez vous connecter avant de continuer."
+ "Access denied. Please log in before continuing.": "Accès refusé. Veuillez vous connecter avant de continuer.",
+ "Unsupported Media Type": "Type de média non supporté",
+ "Upload failed": "L'envoi a échoué"
},
it: {
"Allocation": "Allocazione",
@@ -96,7 +100,9 @@ var locales = {
"The server responded with an error. We have been informed and will investigate the problem.": "Il server ha risposto con un errore. Siamo stati informati e indagheremo sul problema.",
"The server could not be reached. Please try again.": "Impossibile raggiungere il server. Per favore riprova.",
"The site could not be found.": "Impossibile trovare il sito.",
- "Access denied. Please log in before continuing.": "Accesso negato. Effettua il login prima di continuare."
+ "Access denied. Please log in before continuing.": "Accesso negato. Effettua il login prima di continuare.",
+ "Unsupported Media Type": "Tipo di media non supportato",
+ "Upload failed": "L'upload non è andato a buon fine"
}
};
diff --git a/src/onegov/org/assets/js/upload.js b/src/onegov/org/assets/js/upload.js
index 203c450408..905b3491c0 100644
--- a/src/onegov/org/assets/js/upload.js
+++ b/src/onegov/org/assets/js/upload.js
@@ -61,8 +61,15 @@ var Upload = function(element) {
processCommonNodes($(xhr.responseText).appendTo(filelist), true);
}
} else {
+ var errText = locale(xhr.statusText || 'Upload failed') ;
+
bar.find('.meter').css('width', '100%');
- bar.addClass('alert').attr('data-error', xhr.statusText);
+ bar.addClass('has-error').attr('data-error', errText);
+ bar.find('.upload-error').text(errText).attr('aria-hidden', 'false').show();
+ bar.attr('title', errText);
+
+ // keep the bar visible but mark as failed
+ bar.find('.upload-filename').addClass('muted');
}
// eslint-disable-next-line no-use-before-define
@@ -82,9 +89,19 @@ var Upload = function(element) {
};
var queue_upload = function(file) {
- var bar = $('
')
- .attr('data-filename', file.name)
- .prependTo(progress);
+ var bar = $(
+ '' +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ '
'
+ ).attr('data-filename', file.name)
+ .prependTo(progress);
+
+ bar.find('.upload-filename').text(file.name);
+
upload_queue.push({file: file, bar: bar});
};
diff --git a/src/onegov/org/forms/event.py b/src/onegov/org/forms/event.py
index 4f571e1c00..9f8d68dd0f 100644
--- a/src/onegov/org/forms/event.py
+++ b/src/onegov/org/forms/event.py
@@ -21,7 +21,11 @@
from onegov.form.fields import UploadFileWithORMSupport
from onegov.form.utils import get_fields_from_class
from onegov.form.validators import (
- FileSizeLimit, ValidPhoneNumber, ValidFilterFormDefinition)
+ FileSizeLimit,
+ ValidPhoneNumber,
+ ValidFilterFormDefinition,
+ MIME_TYPES_EXCEL
+)
from onegov.form.validators import WhitelistedMimeType
from onegov.gis import CoordinatesField
from onegov.org import _
@@ -537,19 +541,7 @@ class EventImportForm(Form):
label=_('Import'),
validators=[
DataRequired(),
- WhitelistedMimeType({
- 'application/excel',
- 'application/vnd.ms-excel',
- (
- 'application/'
- 'vnd.openxmlformats-officedocument.spreadsheetml.sheet'
- ),
- 'application/vnd.ms-office',
- 'application/octet-stream',
- 'application/zip',
- 'text/csv',
- 'text/plain',
- }),
+ WhitelistedMimeType(MIME_TYPES_EXCEL),
FileSizeLimit(10 * 1024 * 1024)
],
render_kw={'force_simple': True}
diff --git a/src/onegov/org/forms/parliamentarian.py b/src/onegov/org/forms/parliamentarian.py
index b9ccf61ab5..39c44867ef 100644
--- a/src/onegov/org/forms/parliamentarian.py
+++ b/src/onegov/org/forms/parliamentarian.py
@@ -6,7 +6,11 @@
from onegov.form.fields import TranslatedSelectField
from onegov.form.fields import UploadField
from onegov.form.forms import NamedFileForm
-from onegov.form.validators import ValidPhoneNumber
+from onegov.form.validators import (
+ MIME_TYPES_IMAGE,
+ ValidPhoneNumber,
+ WhitelistedMimeType
+)
from onegov.org import _
from onegov.parliament.models.parliamentarian import GENDERS
from wtforms.fields import DateField
@@ -49,6 +53,9 @@ class ParliamentarianForm(NamedFileForm):
picture = UploadField(
label=_('Picture'),
fieldset=_('Basic properties'),
+ validators=[
+ WhitelistedMimeType(MIME_TYPES_IMAGE)
+ ]
)
party = StringField(
diff --git a/src/onegov/org/models/file.py b/src/onegov/org/models/file.py
index 8cf26214a5..d7a0153103 100644
--- a/src/onegov/org/models/file.py
+++ b/src/onegov/org/models/file.py
@@ -12,6 +12,7 @@
from onegov.file import File, FileSet, FileCollection, FileSetCollection
from onegov.file import SearchableFile
from onegov.file.utils import IMAGE_MIME_TYPES_AND_SVG
+from onegov.form.validators import WhitelistedMimeType
from onegov.org import _
from onegov.org.models.extensions import AccessExtension
from onegov.org.utils import widest_access
@@ -312,7 +313,7 @@ class GeneralFileCollection(
GroupFilesByDateMixin[GeneralFile]
):
- supported_content_types = 'all'
+ supported_content_types = WhitelistedMimeType.whitelist
file_list = as_selectable("""
SELECT
diff --git a/src/onegov/org/views/files.py b/src/onegov/org/views/files.py
index f8a4208f81..caf2ed895e 100644
--- a/src/onegov/org/views/files.py
+++ b/src/onegov/org/views/files.py
@@ -442,10 +442,15 @@ def handle_file_upload(
content=fs.file
)
- supported_content_types = getattr(self, 'supported_content_types', 'all')
+ supported_content_types = self.supported_content_types # type:ignore[attr-defined]
if supported_content_types != 'all':
if file.reference.content_type not in supported_content_types:
+ # Fail the post request from upload.js with status code 415
+ # (Unsupported Media Type). Raising the HTTP exception here causes
+ # the request transaction to abort and roll back any previous
+ # changes (including the `self.add(...)` above), so the file won't
+ # be persisted if the content type is unsupported.
raise exc.HTTPUnsupportedMediaType()
return file
diff --git a/src/onegov/pas/forms/data_import.py b/src/onegov/pas/forms/data_import.py
index e5de118dcd..1ec6b24926 100644
--- a/src/onegov/pas/forms/data_import.py
+++ b/src/onegov/pas/forms/data_import.py
@@ -4,6 +4,7 @@
from onegov.core.utils import dictionary_to_binary
from onegov.form import Form
from onegov.form.fields import UploadMultipleField
+from onegov.form.validators import MIME_TYPES_JSON
from onegov.pas import _
from onegov.pas.importer.json_import import (
MembershipData,
@@ -24,6 +25,8 @@ class DataImportForm(Form):
people_source = UploadMultipleField(
label=_('People Data (JSON)'),
description=_('JSON file containing parliamentarian data.'),
+ validators=[], # no validators as files are not stored
+ allowed_mimetypes=tuple(MIME_TYPES_JSON),
)
organizations_source = UploadMultipleField(
label=_('Organizations Data (JSON)'),
@@ -31,6 +34,8 @@ class DataImportForm(Form):
'JSON file containing organization data (commissions, '
'parties, etc.).'
),
+ validators=[], # no validators as files are not stored
+ allowed_mimetypes=tuple(MIME_TYPES_JSON),
)
memberships_source = UploadMultipleField(
label=_('Memberships Data (JSON)'),
@@ -38,6 +43,8 @@ class DataImportForm(Form):
'JSON file containing membership data (who is member of '
'what organization).'
),
+ validators=[], # no validators as files are not stored
+ allowed_mimetypes=tuple(MIME_TYPES_JSON),
)
validate_schema = BooleanField(
diff --git a/src/onegov/pas/forms/parliamentarian.py b/src/onegov/pas/forms/parliamentarian.py
index 0f184186bc..4d0a85563f 100644
--- a/src/onegov/pas/forms/parliamentarian.py
+++ b/src/onegov/pas/forms/parliamentarian.py
@@ -4,7 +4,11 @@
from onegov.form.fields import TranslatedSelectField
from onegov.form.fields import UploadField
from onegov.form.forms import NamedFileForm
-from onegov.form.validators import ValidPhoneNumber
+from onegov.form.validators import (
+ ValidPhoneNumber,
+ MIME_TYPES_IMAGE,
+ WhitelistedMimeType
+)
from onegov.parliament.models.parliamentarian import GENDERS
from onegov.parliament.models.parliamentarian import SHIPPING_METHODS
from onegov.pas.collections.parliamentarian import (
@@ -65,6 +69,9 @@ class PASParliamentarianForm(NamedFileForm):
picture = UploadField(
label=_('Picture'),
fieldset=_('Basic properties'),
+ validators=[
+ WhitelistedMimeType(MIME_TYPES_IMAGE),
+ ]
)
shipping_method = TranslatedSelectField(
diff --git a/src/onegov/pas/views/data_import.py b/src/onegov/pas/views/data_import.py
index 25b16ee0f9..0445ddcd4e 100644
--- a/src/onegov/pas/views/data_import.py
+++ b/src/onegov/pas/views/data_import.py
@@ -95,6 +95,7 @@ def handle_data_import(
error_message = None
+ # FIXME: why not use `form.submitted(request)` like in many places?
if request.method == 'POST' and form.validate():
try:
# Load and concatenate data from uploaded files
diff --git a/src/onegov/town6/templates/files.pt b/src/onegov/town6/templates/files.pt
index 334d010c4c..cea086a882 100644
--- a/src/onegov/town6/templates/files.pt
+++ b/src/onegov/town6/templates/files.pt
@@ -24,6 +24,9 @@
Upload Date
|
+
+ Published until
+ |
diff --git a/src/onegov/town6/theme/styles/upload.scss b/src/onegov/town6/theme/styles/upload.scss
index 40d4fbcacf..5a13e694fc 100644
--- a/src/onegov/town6/theme/styles/upload.scss
+++ b/src/onegov/town6/theme/styles/upload.scss
@@ -39,27 +39,58 @@
margin-bottom: 1.5rem;
.progress {
- margin-left: 25%;
- width: 75%;
+ padding: 0.5rem;
+ background: #f6f6f6;
+ position: relative;
+ overflow: visible;
- &::before {
- content: attr(data-filename);
- font-size: .875rem;
- left: 1rem;
+ .progress-main {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ }
+
+ .upload-filename {
+ flex: 0 0 auto;
+ font-size: 0.95rem;
+ color: #333;
+ max-width: 50%;
+ white-space: nowrap;
overflow: hidden;
- text-align: left;
text-overflow: ellipsis;
- white-space: nowrap;
- width: 22.5%;
}
- &.alert::after {
- color: $white;
- content: attr(data-error);
- font-size: .8rem;
- margin-left: .75ex;
- margin-top: -1.2rem;
- position: absolute;
+ .meter {
+ display: inline-block;
+ height: 10px;
+ background: #0b7db1;
+ border-radius: 6px;
+ flex: 1 1 auto;
+ transition: width 150ms linear;
+ }
+
+ .upload-error {
+ margin-top: 0.4rem;
+ color: #842029;
+ background: #f8d7da;
+ padding: 0.4rem 0.6rem;
+ font-size: 0.875rem;
+ display: none;
+ }
+
+ &.has-error {
+ .meter { background: #b02a37; }
+ .upload-error { display: block; }
+ }
+
+ // optional compact mobile layout
+ @media (max-width: 640px) {
+ .progress-main {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 0.5rem;
+ }
+ .upload-filename { max-width: 100%; }
}
}
-}
+}
\ No newline at end of file
diff --git a/src/onegov/translator_directory/collections/documents.py b/src/onegov/translator_directory/collections/documents.py
index 9fbcceba17..68ea1c7613 100644
--- a/src/onegov/translator_directory/collections/documents.py
+++ b/src/onegov/translator_directory/collections/documents.py
@@ -4,6 +4,7 @@
from itertools import groupby
from onegov.file import File, FileCollection
+from onegov.form.validators import WhitelistedMimeType
from onegov.translator_directory.models.translator import Translator
@@ -29,7 +30,7 @@
class TranslatorDocumentCollection(FileCollection[File]):
- supported_content_types = 'all'
+ supported_content_types = WhitelistedMimeType.whitelist
def __init__(
self,
diff --git a/src/onegov/translator_directory/forms/accreditation.py b/src/onegov/translator_directory/forms/accreditation.py
index c54c2500ea..0c07f83c06 100644
--- a/src/onegov/translator_directory/forms/accreditation.py
+++ b/src/onegov/translator_directory/forms/accreditation.py
@@ -14,7 +14,7 @@
from onegov.form.fields import PanelField
from onegov.form.fields import TagsField
from onegov.form.fields import UploadField
-from onegov.form.validators import FileSizeLimit
+from onegov.form.validators import FileSizeLimit, MIME_TYPES_PDF
from onegov.form.validators import Stdnum
from onegov.form.validators import StrictOptional
from onegov.form.validators import ValidPhoneNumber
@@ -356,7 +356,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
declaration_of_authorization = UploadField(
label=_('Signed declaration of authorization (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -367,7 +367,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
letter_of_motivation = UploadField(
label=_('Short letter of motivation (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -378,7 +378,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
resume = UploadField(
label=_('Resume (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -393,7 +393,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
'level C2 are mandatory for non-native speakers.'
),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -404,7 +404,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
social_security_card = UploadField(
label=_('Social security card (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -415,7 +415,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
passport = UploadField(
label=_('Identity card, passport or foreigner identity card (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -426,7 +426,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
passport_photo = UploadField(
label=_('Current passport photo (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -438,7 +438,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
label=_('Current extract from the debt collection register (PDF)'),
description=_('Maximum 6 months since issue.'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -453,7 +453,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
'www.strafregister.admin.ch'
),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -465,7 +465,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
label=_('Certificate of Capability (PDF)'),
description=_('Available from the municipal or city administration.'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
@@ -479,7 +479,7 @@ class RequestAccreditationForm(Form, DrivingDistanceMixin):
'self-employment'
),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
DataRequired(),
],
diff --git a/src/onegov/translator_directory/forms/mutation.py b/src/onegov/translator_directory/forms/mutation.py
index f32560861b..052ae20b39 100644
--- a/src/onegov/translator_directory/forms/mutation.py
+++ b/src/onegov/translator_directory/forms/mutation.py
@@ -12,7 +12,7 @@
from onegov.form.fields import MultiCheckboxField
from onegov.form.fields import TagsField
from onegov.form.fields import UploadField
-from onegov.form.validators import FileSizeLimit
+from onegov.form.validators import FileSizeLimit, MIME_TYPES_PDF
from onegov.form.validators import Stdnum
from onegov.form.validators import ValidPhoneNumber
from onegov.form.validators import ValidSwissSocialSecurityNumber
@@ -256,7 +256,7 @@ def as_file(field: UploadField, category: str) -> File | None:
declaration_of_authorization = UploadField(
label=_('Signed declaration of authorization (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -267,7 +267,7 @@ def as_file(field: UploadField, category: str) -> File | None:
letter_of_motivation = UploadField(
label=_('Short letter of motivation (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -278,7 +278,7 @@ def as_file(field: UploadField, category: str) -> File | None:
resume = UploadField(
label=_('Resume (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -293,7 +293,7 @@ def as_file(field: UploadField, category: str) -> File | None:
'level C2 are mandatory for non-native speakers.'
),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -304,7 +304,7 @@ def as_file(field: UploadField, category: str) -> File | None:
social_security_card = UploadField(
label=_('Social security card (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -315,7 +315,7 @@ def as_file(field: UploadField, category: str) -> File | None:
passport = UploadField(
label=_('Identity card, passport or foreigner identity card (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -326,7 +326,7 @@ def as_file(field: UploadField, category: str) -> File | None:
passport_photo = UploadField(
label=_('Current passport photo (PDF)'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -338,7 +338,7 @@ def as_file(field: UploadField, category: str) -> File | None:
label=_('Current extract from the debt collection register (PDF)'),
description=_('Maximum 6 months since issue.'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -353,7 +353,7 @@ def as_file(field: UploadField, category: str) -> File | None:
'www.strafregister.admin.ch'
),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -365,7 +365,7 @@ def as_file(field: UploadField, category: str) -> File | None:
label=_('Certificate of Capability (PDF)'),
description=_('Available from the municipal or city administration.'),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
@@ -379,7 +379,7 @@ def as_file(field: UploadField, category: str) -> File | None:
'self-employment'
),
validators=[
- WhitelistedMimeType({'application/pdf'}),
+ WhitelistedMimeType(MIME_TYPES_PDF),
FileSizeLimit(100 * 1024 * 1024),
Optional(),
],
diff --git a/tests/onegov/form/test_fields.py b/tests/onegov/form/test_fields.py
index d3dfc55984..483dfb2dce 100644
--- a/tests/onegov/form/test_fields.py
+++ b/tests/onegov/form/test_fields.py
@@ -6,6 +6,7 @@
from cgi import FieldStorage
from copy import deepcopy
from datetime import datetime
+
from onegov.core.utils import Bunch
from onegov.core.utils import dictionary_to_binary
from onegov.form import Form
@@ -22,15 +23,36 @@
from onegov.form.fields import UploadField
from onegov.form.fields import UploadMultipleField
from onegov.form.fields import URLField
-from onegov.form.validators import ValidPhoneNumber
+from onegov.form.validators import (
+ ValidPhoneNumber, WhitelistedMimeType, MIME_TYPES_JSON, MIME_TYPES_PDF)
from unittest.mock import patch
+from wtforms import FileField, Field
from wtforms.validators import Optional
from wtforms.validators import URL
-from typing import Any, TYPE_CHECKING
+from typing import Any, TYPE_CHECKING, Self, Sequence
+
if TYPE_CHECKING:
from webob.request import _FieldStorageWithFile
+ from onegov.form.types import (
+ FormT, Validators, Validator)
+
+
+def assert_whitelisted_mimetype_validator(
+ field: UploadField | UploadMultipleField
+) -> None:
+
+ validator = find_validator(field, WhitelistedMimeType)
+ assert validator
+ assert validator.whitelist == WhitelistedMimeType.whitelist # type:ignore[attr-defined]
+
+
+def find_validator(
+ field: Field | FileField,
+ cls: type
+) -> Validator[Any, Any] | None:
+ return next((v for v in field.validators if isinstance(v, cls)), None)
class DummyPostData(dict[str, Any]):
@@ -56,9 +78,15 @@ def create_file(
def test_upload_field() -> None:
- def create_field() -> tuple[Form, UploadField]:
+ def create_field(
+ validators: Validators[FormT, Self] | None = None, # type:ignore[misc]
+ allowed_mimetypes: Sequence[str] | None = None,
+ ) -> tuple[Form, UploadField]:
form = Form()
- field = UploadField()
+ field = UploadField(
+ validators=validators,
+ allowed_mimetypes=allowed_mimetypes,
+ )
field = field.bind(form, 'upload') # type: ignore[attr-defined]
return form, field
@@ -68,12 +96,16 @@ def create_field() -> tuple[Form, UploadField]:
assert data == {}
assert field.file is None
assert field.filename is None
+ assert field.validate(form)
+ assert_whitelisted_mimetype_validator(field)
form, field = create_field()
data = field.process_fieldstorage('')
assert data == {}
assert field.file is None
assert field.filename is None
+ assert field.validate(form)
+ assert_whitelisted_mimetype_validator(field)
textfile = create_file('text/plain', 'foo.txt', b'foo')
data = field.process_fieldstorage(textfile)
@@ -84,6 +116,8 @@ def create_field() -> tuple[Form, UploadField]:
assert dictionary_to_binary(data) == b'foo' # type: ignore[arg-type]
assert field.filename == 'foo.txt'
assert field.file.read() == b'foo' # type: ignore[attr-defined]
+ assert field.validate(form)
+ assert_whitelisted_mimetype_validator(field)
form, field = create_field()
textfile = create_file('text/plain', 'C:/mydata/bar.txt', b'bar')
@@ -95,6 +129,20 @@ def create_field() -> tuple[Form, UploadField]:
assert dictionary_to_binary(data) == b'bar' # type: ignore[arg-type]
assert field.filename == 'bar.txt'
assert field.file.read() == b'bar' # type: ignore[union-attr]
+ assert field.validate(form)
+ assert_whitelisted_mimetype_validator(field)
+
+ # failing mime type validator
+ form, field = create_field(allowed_mimetypes=tuple(MIME_TYPES_PDF))
+ textfile = create_file('text/plain', 'baz.txt', b'baz')
+ data = field.data = field.process_fieldstorage(textfile)
+ assert data['filename'] == 'baz.txt'
+ assert data['mimetype'] == 'text/plain'
+ validator = find_validator(field, WhitelistedMimeType)
+ assert validator
+ assert validator.whitelist == ('application/pdf',) # type:ignore[attr-defined]
+ assert not field.validate(form)
+ assert 'Files of this type are not supported.' in field.errors
# Test rendering
form, field = create_field()
@@ -104,6 +152,7 @@ def create_field() -> tuple[Form, UploadField]:
field.data = field.process_fieldstorage(textfile)
assert 'without-data' in field(force_simple=True)
+ assert_whitelisted_mimetype_validator(field)
html = field()
assert 'with-data' in html
@@ -111,6 +160,7 @@ def create_field() -> tuple[Form, UploadField]:
assert 'keep' in html
assert 'type="file"' in html
assert 'value="baz.txt"' not in html
+ assert_whitelisted_mimetype_validator(field)
html = field(resend_upload=True)
assert 'with-data' in html
@@ -118,12 +168,14 @@ def create_field() -> tuple[Form, UploadField]:
assert 'keep' in html
assert 'type="file"' in html
assert 'value="baz.txt"' in html
+ assert_whitelisted_mimetype_validator(field)
# Test submit
form, field = create_field()
field.process(DummyPostData({}))
assert field.validate(form)
assert field.data == {}
+ assert_whitelisted_mimetype_validator(field)
form, field = create_field()
field.process(DummyPostData({'upload': 'abcd'}))
@@ -132,6 +184,7 @@ def create_field() -> tuple[Form, UploadField]:
assert field.data == {}
assert field.file is None
assert field.filename is None
+ assert_whitelisted_mimetype_validator(field)
# ... simple
form, field = create_field()
@@ -146,6 +199,7 @@ def create_field() -> tuple[Form, UploadField]:
assert dictionary_to_binary(field.data) == b'foobar' # type: ignore[arg-type]
assert field.filename == 'foobar.txt'
assert field.file.read() == b'foobar' # type: ignore[union-attr]
+ assert_whitelisted_mimetype_validator(field)
# ... with select
form, field = create_field()
@@ -153,6 +207,7 @@ def create_field() -> tuple[Form, UploadField]:
field.process(DummyPostData({'upload': ['keep', textfile]}))
assert field.validate(form)
assert field.action == 'keep'
+ assert_whitelisted_mimetype_validator(field)
form, field = create_field()
textfile = create_file('text/plain', 'foobar.txt', b'foobar')
@@ -160,6 +215,7 @@ def create_field() -> tuple[Form, UploadField]:
assert field.validate(form)
assert field.action == 'delete'
assert field.data == {}
+ assert_whitelisted_mimetype_validator(field)
form, field = create_field()
textfile = create_file('text/plain', 'foobar.txt', b'foobar')
@@ -173,6 +229,7 @@ def create_field() -> tuple[Form, UploadField]:
assert dictionary_to_binary(field.data) == b'foobar' # type: ignore[arg-type]
assert field.filename == 'foobar.txt'
assert field.file.read() == b'foobar' # type: ignore[union-attr]
+ assert_whitelisted_mimetype_validator(field)
# ... with select and keep upload
previous = field.data
@@ -191,6 +248,7 @@ def create_field() -> tuple[Form, UploadField]:
assert field.data['mimetype'] == 'text/plain'
assert field.data['size'] == 6
assert dictionary_to_binary(field.data) == b'foobar' # type: ignore[arg-type]
+ assert_whitelisted_mimetype_validator(field)
field.process(DummyPostData({'upload': [
'delete',
@@ -203,6 +261,7 @@ def create_field() -> tuple[Form, UploadField]:
assert field2.validate(form)
assert field2.action == 'delete'
assert field2.data == {}
+ assert_whitelisted_mimetype_validator(field)
field.process(DummyPostData({'upload': [
'replace',
@@ -221,15 +280,35 @@ def create_field() -> tuple[Form, UploadField]:
assert dictionary_to_binary(field2.data) == b'foobaz' # type: ignore[arg-type]
assert field2.filename == 'foobaz.txt'
assert field2.file.read() == b'foobaz' # type: ignore[union-attr]
+ assert_whitelisted_mimetype_validator(field)
def test_upload_multiple_field() -> None:
- def create_field() -> tuple[Form, UploadMultipleField]:
+ def create_field(
+ validators: Validators[FormT, Self] | None = None, # type:ignore[misc]
+ allowed_mimetypes: Sequence[str] | None = None,
+ ) -> tuple[Form, UploadMultipleField]:
form = Form()
- field = UploadMultipleField()
+ field = UploadMultipleField(
+ validators=validators,
+ allowed_mimetypes=allowed_mimetypes,
+ )
field = field.bind(form, 'uploads') # type: ignore[attr-defined]
return form, field
+ # failing mime type validator
+ form, field = create_field(allowed_mimetypes=tuple(MIME_TYPES_JSON))
+ file1 = create_file('text/plain', 'baz.txt', b'baz')
+ field.process(DummyPostData({'uploads': [file1]}))
+ assert not field.validate(form)
+ assert len(field.data) == 1
+ assert field.data[0]['filename'] == 'baz.txt'
+ assert field.data[0]['mimetype'] == 'text/plain'
+ for subfield in field:
+ validator = find_validator(subfield, WhitelistedMimeType)
+ assert validator
+ assert validator.whitelist == ('application/json',) # type:ignore[attr-defined]
+
# Test rendering and initial submit
form, field = create_field()
field.process(None)
@@ -245,6 +324,7 @@ def create_field() -> tuple[Form, UploadMultipleField]:
file1 = create_file('text/plain', 'baz.txt', b'baz')
file2 = create_file('text/plain', 'foobar.txt', b'foobar')
field.process(DummyPostData({'uploads': [file1, file2]}))
+ assert field.validate(form)
assert len(field.data) == 2
assert field.data[0]['filename'] == 'baz.txt'
assert field.data[0]['mimetype'] == 'text/plain'
@@ -265,8 +345,11 @@ def create_field() -> tuple[Form, UploadMultipleField]:
assert dictionary_to_binary(file_field2.data) == b'foobar' # type: ignore[arg-type]
assert file_field2.filename == 'foobar.txt'
assert file_field2.file.read() == b'foobar' # type: ignore[union-attr]
+ for subfield in field:
+ assert_whitelisted_mimetype_validator(subfield)
html = field(force_simple=True)
+ assert field.validate(form)
assert 'without-data' in html
assert 'multiple' in html
assert 'name="uploads"' in html
@@ -274,6 +357,7 @@ def create_field() -> tuple[Form, UploadMultipleField]:
assert 'name="uploads-0"' not in html
html = field()
+ assert field.validate(form)
assert 'with-data' in html
assert 'name="uploads-0"' in html
assert 'Uploaded file: baz.txt (3 Bytes) ✓' in html
@@ -290,6 +374,7 @@ def create_field() -> tuple[Form, UploadMultipleField]:
assert 'multiple' in html
html = field(resend_upload=True)
+ assert field.validate(form)
assert 'with-data' in html
assert 'Uploaded file: baz.txt (3 Bytes) ✓' in html
assert 'Uploaded file: foobar.txt (6 Bytes) ✓' in html
diff --git a/tests/onegov/form/test_parser.py b/tests/onegov/form/test_parser.py
index 003314888e..5d48f6c912 100644
--- a/tests/onegov/form/test_parser.py
+++ b/tests/onegov/form/test_parser.py
@@ -4,6 +4,9 @@
from dateutil.relativedelta import relativedelta
from decimal import Decimal
+
+from wtforms import Field
+
from onegov.form import Form, errors, find_field
from onegov.form import parse_formcode, parse_form, flatten_fieldsets
from onegov.form.errors import (
@@ -12,10 +15,20 @@
InvalidCommentLocationSyntax,
)
from onegov.form.fields import (
- DateTimeLocalField, MultiCheckboxField, TimeField, URLField, VideoURLField)
+ DateTimeLocalField,
+ MultiCheckboxField,
+ TimeField,
+ URLField,
+ VideoURLField,
+)
from onegov.form.parser.grammar import field_help_identifier
-from onegov.form.validators import LaxDataRequired
-from onegov.form.validators import ValidDateRange
+from onegov.form.validators import (
+ LaxDataRequired,
+ WhitelistedMimeType,
+ FileSizeLimit,
+ StrictOptional,
+ ValidDateRange
+)
from onegov.pay import Price
from textwrap import dedent
from webob.multidict import MultiDict
@@ -28,16 +41,25 @@
from wtforms.validators import Optional
from wtforms.validators import Regexp
+from typing import TYPE_CHECKING, Any
-from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pyparsing import ParserElement, ParseResults
+ from onegov.form.types import Validator
+
def parse(expr: ParserElement, text: str) -> ParseResults:
return expr.parseString(text)
+def find_validator(
+ field: Field | FileField,
+ cls: type
+) -> Validator[Any, Any] | None:
+ return next((v for v in field.validators if isinstance(v, cls)), None)
+
+
@pytest.mark.parametrize('comment,output', [
('<< Some text >>', 'Some text'),
('<< [Z](www.co.me) >>', '[Z](www.co.me)')
@@ -345,6 +367,27 @@ def test_parse_fileinput() -> None:
assert isinstance(form['file'], FileField)
assert form['file'].widget.multiple is False # type: ignore[attr-defined]
+ # verify attached mime type validator
+ assert form['file'].validators
+ validator = find_validator(form['file'], WhitelistedMimeType)
+ assert validator
+ assert validator.whitelist == { # type:ignore[attr-defined]
+ 'application/msword', 'application/pdf'
+ }
+ assert find_validator(form['file'], FileSizeLimit)
+
+ form = parse_form("File *= *.*")()
+ assert form['file'].validators
+ validator = find_validator(form['file'], WhitelistedMimeType)
+ assert validator
+ assert validator.whitelist == WhitelistedMimeType.whitelist # type:ignore[attr-defined]
+ assert find_validator(form['file'], FileSizeLimit)
+
+ # ensure nickname field did not get validators from the upload field
+ form = parse_form("Nickname = ___\nFile = *.pdf|*.doc")()
+ assert find_validator(form['nickname'], StrictOptional)
+ assert not find_validator(form['nickname'], WhitelistedMimeType)
+
def test_parse_multiplefileinput() -> None:
form = parse_form("Files = *.pdf|*.doc (multiple)")()
@@ -353,9 +396,24 @@ def test_parse_multiplefileinput() -> None:
assert isinstance(form['files'], FileField)
assert form['files'].widget.multiple is True # type: ignore[attr-defined]
+ # verify attached mime type validator
+ assert form['files'].validators
+ validator = find_validator(form['files'], WhitelistedMimeType)
+ assert validator
+ assert validator.whitelist == { # type:ignore[attr-defined]
+ 'application/msword', 'application/pdf'
+ }
+ assert find_validator(form['files'], FileSizeLimit)
+
+ form = parse_form("My files *= *.* (multiple)")()
+ assert form['my_files'].validators
+ validator = find_validator(form['my_files'], WhitelistedMimeType)
+ assert validator
+ assert validator.whitelist == WhitelistedMimeType.whitelist # type:ignore[attr-defined]
+ assert find_validator(form['my_files'], FileSizeLimit)
-def test_parse_radio() -> None:
+def test_parse_radio() -> None:
text = dedent("""
Gender =
( ) Male
@@ -543,6 +601,59 @@ def test_dependent_validation() -> None:
)
)
+ # dependency with upload field
+ text = dedent("""
+ method *=
+ (x) Drop off
+ Estimated drop off time = HH:MM
+ ( ) Upload
+ Attachment *= *.pdf|*.doc
+ """)
+
+ form_class = parse_form(text)
+
+ # drop off: no file required
+ form = form_class(MultiDict([('method', 'Drop off')]))
+ assert form.validate()
+ assert not form.errors
+
+ # upload selected but no file provided
+ form = form_class(MultiDict([('method', 'Upload')]))
+ assert not form.validate()
+ assert form.errors == {'method_attachment': ['This field is required.']}
+
+ # upload selected with large attachment and wrong type
+ from io import BytesIO
+ from werkzeug.datastructures import FileStorage
+
+ fs_text = FileStorage(
+ stream=BytesIO(b'Hello, this is plain text\n' +
+ b'A' * 101 * 1000 ** 2),
+ filename='test.txt',
+ content_type='text/plain'
+ )
+ form = form_class(
+ MultiDict([('method', 'Upload'), ('method_attachment', fs_text)]))
+ assert not form.validate()
+ assert form.errors == {
+ 'method_attachment': [
+ 'Files of this type are not supported.',
+ 'The file is too large, please provide a file smaller '
+ 'than 100.0 MB.'
+ ]
+ }
+
+ # upload selected with large attachment and wrong type
+ fs_pdf = FileStorage(
+ stream=BytesIO(b'%PDF-1.4\n%'),
+ filename='test.pdf',
+ content_type='application/pdf'
+ )
+ form = form_class(
+ MultiDict([('method', 'Upload'), ('method_attachment', fs_pdf)]))
+ assert form.validate()
+ assert not form.errors
+
def test_nested_regression() -> None:
diff --git a/tests/onegov/pas/test_views.py b/tests/onegov/pas/test_views.py
index e1bc2c6ff1..f03a349972 100644
--- a/tests/onegov/pas/test_views.py
+++ b/tests/onegov/pas/test_views.py
@@ -382,7 +382,6 @@ def do_upload_procedure(
result = page.form.submit().maybe_follow()
# Add assertions as needed
- assert result.status_code == 200
assert result.status_code == 200, f"Import failed: {result.text}"
return result