Skip to content

Commit

Permalink
add file type validation
Browse files Browse the repository at this point in the history
  • Loading branch information
depsiatwal committed Jan 23, 2025
1 parent 7699fd9 commit ee9e36a
Show file tree
Hide file tree
Showing 21 changed files with 109 additions and 46 deletions.
3 changes: 3 additions & 0 deletions conf/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@
"image/tiff",
),
)
ACCEPTED_FILE_UPLOAD_EXTENSIONS = env.list(
"ACCEPTED_FILE_UPLOAD_EXTENSIONS", default=["pdf", "doc", "docx", "jpg", "pnf", "odt", "txt"]
)
# AV is performed by the API, but these empty settings are required by django-chunk-s3-av-upload-handlers
CLAM_AV_USERNAME = env.str("CLAM_AV_USERNAME", "")
CLAM_AV_PASSWORD = env.str("CLAM_AV_PASSWORD", "")
Expand Down
9 changes: 6 additions & 3 deletions core/file_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import magic

from django.conf import settings
from django.core.files.uploadhandler import UploadFileException
from django.core.files.uploadhandler import UploadFileException, SkipFile
from django.http import StreamingHttpResponse

from django_chunk_upload_handlers.s3 import S3FileUploadHandler
Expand Down Expand Up @@ -62,7 +62,10 @@ def receive_data_chunk(self, raw_data, start):
mime = magic.from_buffer(raw_data, mime=True)
if mime not in self.ACCEPTED_FILE_UPLOAD_MIME_TYPES:
self.abort()
raise UnacceptableMimeTypeError(f"Unsupported file type: {mime}")
# Raise SkipFile signals the django multipartparser to exhaust the stream and stops the upload for this file.
exception = UnacceptableMimeTypeError(f"Unsupported file type: {mime}")
logger.error(exception)
raise exception
super().receive_data_chunk(raw_data, start)

def file_complete(self, *args, **kwargs):
Expand All @@ -82,7 +85,7 @@ class UploadFailed(UploadFileException):
pass


class UnacceptableMimeTypeError(UploadFailed):
class UnacceptableMimeTypeError(SkipFile):
pass


Expand Down
9 changes: 8 additions & 1 deletion exporter/applications/forms/parties.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
from django.core.validators import MaxLengthValidator, URLValidator
from django.urls import reverse_lazy


from core.common.forms import BaseForm
from core.forms.layouts import ConditionalRadios, ConditionalRadiosQuestion
from core.forms.widgets import Autocomplete
from exporter.core.constants import CaseTypes, FileUploadFileTypes
from exporter.core.validators import FileExtensionValidator

from exporter.core.services import get_countries
from lite_content.lite_exporter_frontend import strings
from lite_content.lite_exporter_frontend.applications import PartyForm, PartyTypeForm
Expand Down Expand Up @@ -432,7 +435,9 @@ class PartyDocumentUploadForm(forms.Form):
error_messages={
"required": "Select an end-user document",
},
validators=[FileExtensionValidator()],
)

product_differences_note = forms.CharField(
widget=forms.Textarea(attrs={"rows": "5"}),
label="Describe any differences between products listed in the document and products on the application (optional)",
Expand Down Expand Up @@ -491,6 +496,7 @@ class PartyEnglishTranslationDocumentUploadForm(forms.Form):
error_messages={
"required": "Select an English translation",
},
validators=[FileExtensionValidator()],
)

def __init__(self, edit, *args, **kwargs):
Expand Down Expand Up @@ -519,6 +525,7 @@ class PartyCompanyLetterheadDocumentUploadForm(forms.Form):
error_messages={
"required": "Select a document on company letterhead",
},
validators=[FileExtensionValidator()],
)

def __init__(self, edit, *args, **kwargs):
Expand All @@ -542,7 +549,7 @@ def get_title(self):

class PartyEC3DocumentUploadForm(forms.Form):
title = "Upload an EC3 form (optional)"
party_ec3_document = forms.FileField(label="", required=False)
party_ec3_document = forms.FileField(label="", required=False, validators=[FileExtensionValidator()])
ec3_missing_reason = forms.CharField(
widget=forms.Textarea(attrs={"rows": "5"}),
label="",
Expand Down
11 changes: 11 additions & 0 deletions exporter/core/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from django.core.exceptions import ValidationError
from django.utils import timezone
from django.conf import settings
from django.core.validators import FileExtensionValidator

from exporter.applications.helpers.date_fields import format_date

Expand Down Expand Up @@ -52,3 +54,12 @@ def __init__(self, message, **kwargs):
def __call__(self, value):
if value > (date.today() + self.relativedelta):
raise ValidationError(self.message)


class FileExtensionValidator(FileExtensionValidator):

FILE_TYPE_ERROR_MSG = "The file type is not supported. Upload a supported file type"
ACCEPTED_FILE_UPLOAD_EXTENSIONS = settings.ACCEPTED_FILE_UPLOAD_EXTENSIONS

def __init__(self):
super().__init__(self.ACCEPTED_FILE_UPLOAD_EXTENSIONS, self.FILE_TYPE_ERROR_MSG)
3 changes: 2 additions & 1 deletion exporter/goods/forms/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
get_pv_gradings_v2,
get_units,
)
from exporter.core.validators import PastDateValidator
from exporter.core.validators import FileExtensionValidator, PastDateValidator
from exporter.core.constants import ProductSecurityFeatures, FileUploadFileTypes


Expand Down Expand Up @@ -363,6 +363,7 @@ class Layout:
error_messages={
"required": "Select a document that shows what your product is designed to do",
},
validators=[FileExtensionValidator()],
)
description = forms.CharField(
widget=forms.Textarea(attrs={"rows": "5"}),
Expand Down
3 changes: 3 additions & 0 deletions exporter/goods/forms/firearms.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
PotentiallyUnsafeClearableFileInput,
)
from exporter.core.validators import (
FileExtensionValidator,
FutureDateValidator,
PastDateValidator,
RelativeDeltaDateValidator,
Expand Down Expand Up @@ -223,6 +224,7 @@ class Layout:
"required": "Select a registered firearms dealer certificate",
},
widget=PotentiallyUnsafeClearableFileInput,
validators=[FileExtensionValidator()],
)

reference_code = forms.CharField(
Expand Down Expand Up @@ -333,6 +335,7 @@ class Layout:
widget=PotentiallyUnsafeClearableFileInput(
force_required=True,
),
validators=[FileExtensionValidator()],
)

section_certificate_number = forms.CharField(
Expand Down
2 changes: 2 additions & 0 deletions exporter/goods/forms/goods.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
)
from exporter.core.helpers import convert_control_list_entries, str_to_bool, convert_control_list_entries_to_options
from exporter.core.services import get_control_list_entries, get_pv_gradings, get_units
from exporter.core.validators import FileExtensionValidator
from exporter.goods.helpers import get_category_display_string, good_summary
from core.common.forms import BaseForm
from lite_content.lite_exporter_frontend.goods import (
Expand Down Expand Up @@ -1112,6 +1113,7 @@ class AttachFirearmsDealerCertificateForm(forms.Form):
error_messages={
"required": "Select certificate file to upload",
},
validators=[FileExtensionValidator()],
)

reference_code = forms.CharField(
Expand Down
3 changes: 3 additions & 0 deletions exporter/organisation/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from crispy_forms_gds.layout import Submit, Layout, HTML

from exporter.core.constants import FileUploadFileTypes
from exporter.core.validators import FileExtensionValidator


def validate_expiry_date(value):
Expand All @@ -28,6 +29,7 @@ class Layout:
label="",
help_text="The file must be smaller than 50MB",
error_messages={"required": "Select certificate file to upload"},
validators=[FileExtensionValidator()],
)
reference_code = forms.CharField(
label="Certificate number",
Expand Down Expand Up @@ -63,6 +65,7 @@ class Layout:
label=FileUploadFileTypes.UPLOAD_GUIDANCE_TEXT,
help_text="The file must be smaller than 50MB",
error_messages={"required": "Select certificate file to upload"},
validators=[FileExtensionValidator()],
)
reference_code = forms.CharField(
label="Certificate number",
Expand Down
34 changes: 31 additions & 3 deletions unit_tests/exporter/applications/forms/test_parties.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,25 @@ def test_party_documents_form(data, valid, errors):
(
(
{"description": "test", "document_in_english": "True", "document_on_letterhead": "True"},
{"party_document": SimpleUploadedFile("test", b"test_content")},
{"party_document": SimpleUploadedFile("test.pdf", b"test_content")},
False,
True,
None,
),
(
{"description": "test", "document_in_english": "True", "document_on_letterhead": "True"},
{"party_document": SimpleUploadedFile("test.xml", b"test_content")},
False,
False,
{"party_document": ["The file type is not supported. Upload a supported file type"]},
),
(
{"description": "test", "document_in_english": "True", "document_on_letterhead": "True"},
{"party_document": SimpleUploadedFile("test.obs", b"test_content")},
False,
False,
{"party_document": ["The file type is not supported. Upload a supported file type"]},
),
(
{"description": "test", "document_in_english": "True", "document_on_letterhead": "True"},
{},
Expand Down Expand Up @@ -361,11 +375,18 @@ def test_party_document_upload_form(data, files, edit, valid, errors):
(
(
{},
{"party_eng_translation_document": SimpleUploadedFile("test", b"test_content")},
{"party_eng_translation_document": SimpleUploadedFile("test.pdf", b"test_content")},
False,
True,
None,
),
(
{},
{"party_eng_translation_document": SimpleUploadedFile("test.pdd", b"test_content")},
False,
False,
{"party_eng_translation_document": ["The file type is not supported. Upload a supported file type"]},
),
(
{},
{},
Expand Down Expand Up @@ -398,11 +419,18 @@ def test_party_english_translation_document_upload_form(data, files, edit, valid
(
(
{},
{"party_letterhead_document": SimpleUploadedFile("test", b"test_content")},
{"party_letterhead_document": SimpleUploadedFile("test.pdf", b"test_content")},
False,
True,
None,
),
(
{},
{"party_letterhead_document": SimpleUploadedFile("test.ioi", b"test_content")},
False,
False,
{"party_letterhead_document": ["The file type is not supported. Upload a supported file type"]},
),
(
{},
{},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ def test_add_good_component_accessory_end_to_end(

response = post_to_step(
AddGoodComponentSteps.PRODUCT_DOCUMENT_UPLOAD,
{"product_document": SimpleUploadedFile("data sheet", b"This is a detailed spec of this Rifle")},
{"product_document": SimpleUploadedFile("data_sheet.pdf", b"This is a detailed spec of this Rifle")},
)

assert response.status_code == 200
Expand Down Expand Up @@ -227,7 +227,7 @@ def test_add_good_component_accessory_end_to_end(
}
assert post_good_document_matcher.called_once
assert post_good_document_matcher.last_request.json() == [
{"name": "data sheet", "s3_key": "data sheet", "size": 0, "description": ""}
{"name": "data_sheet.pdf", "s3_key": "data_sheet.pdf", "size": 0, "description": ""}
]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def good_on_application(data_standard_case):
@pytest.fixture
def product_document():
return {
"product_document": SimpleUploadedFile("data sheet", b"This is a detailed spec of this Rifle"),
"product_document": SimpleUploadedFile("data_sheet.pdf", b"This is a detailed spec of this Rifle"),
"description": "product data sheet",
}

Expand Down Expand Up @@ -474,7 +474,7 @@ def test_edit_product_document_availability_upload_new_document(
assert document_delete_request.matcher.called_once

assert requests_mock.request_history.pop().json() == [
{"name": "data sheet", "s3_key": "data sheet", "size": 0, "description": "product data sheet"}
{"name": "data_sheet.pdf", "s3_key": "data_sheet.pdf", "size": 0, "description": "product data sheet"}
]

assert requests_mock.request_history.pop().json() == {
Expand Down Expand Up @@ -520,7 +520,7 @@ def test_upload_new_product_document_to_replace_existing_one(
assert document_delete_request.matcher.called_once

assert requests_mock.request_history.pop().json() == [
{"name": "data sheet", "s3_key": "data sheet", "size": 0, "description": "product data sheet"}
{"name": "data_sheet.pdf", "s3_key": "data_sheet.pdf", "size": 0, "description": "product data sheet"}
]

assert requests_mock.request_history.pop().json() == {"is_document_sensitive": False}
Expand Down Expand Up @@ -567,5 +567,5 @@ def test_edit_product_document_upload_form(
assert document_delete_request.matcher.called_once

assert requests_mock.last_request.json() == [
{"name": "data sheet", "s3_key": "data sheet", "size": 0, "description": "product data sheet"}
{"name": "data_sheet.pdf", "s3_key": "data_sheet.pdf", "size": 0, "description": "product data sheet"}
]
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def test_add_good_firearm_product_document_available_but_not_sensitive(

response = post_to_step(
AddGoodFirearmSteps.PRODUCT_DOCUMENT_UPLOAD,
{"product_document": SimpleUploadedFile("data sheet", b"This is a detailed spec of this Rifle")},
{"product_document": SimpleUploadedFile("data_sheet.pdf", b"This is a detailed spec of this Rifle")},
)
assert response.status_code == 200
assert isinstance(response.context["form"], FirearmCategoryForm)
Expand Down Expand Up @@ -452,7 +452,7 @@ def test_add_good_firearm_with_rfd_document_submission(
)
response = post_to_step(
AddGoodFirearmSteps.PRODUCT_DOCUMENT_UPLOAD,
{"product_document": SimpleUploadedFile("data sheet", b"This is a detailed spec of this Rifle")},
{"product_document": SimpleUploadedFile("data_sheet.pdf", b"This is a detailed spec of this Rifle")},
)

assert response.status_code == 302
Expand Down Expand Up @@ -497,7 +497,9 @@ def test_add_good_firearm_with_rfd_document_submission(

assert post_good_document_matcher.called_once
good_doc_request = post_good_document_matcher.last_request
assert good_doc_request.json() == [{"name": "data sheet", "s3_key": "data sheet", "size": 0, "description": ""}]
assert good_doc_request.json() == [
{"name": "data_sheet.pdf", "s3_key": "data_sheet.pdf", "size": 0, "description": ""}
]

assert post_applications_document_matcher.called_once
application_doc_request = post_applications_document_matcher.last_request
Expand Down Expand Up @@ -701,7 +703,7 @@ def test_add_good_firearm_without_rfd_document_submission_registered_firearms_de
{"is_covered_by_section_5": "no"},
)
expiry_date = datetime.date.today() + datetime.timedelta(days=5)
file_name = "test"
file_name = "test.pdf"
post_to_step(
AddGoodFirearmSteps.ATTACH_RFD_CERTIFICATE,
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def edit_product_sensitivity_url(application, good_on_application):
@pytest.fixture(autouse=True)
def product_document():
return {
"product_document": SimpleUploadedFile("data sheet", b"This is a detailed spec of this Rifle"),
"product_document": SimpleUploadedFile("data_sheet.pdf", b"This is a detailed spec of this Rifle"),
"description": "product data sheet",
}

Expand Down Expand Up @@ -302,7 +302,7 @@ def test_edit_product_document_upload_form(
assert document_delete_request.matcher.called_once

assert requests_mock.last_request.json() == [
{"name": "data sheet", "s3_key": "data sheet", "size": 0, "description": "product data sheet"}
{"name": "data_sheet.pdf", "s3_key": "data_sheet.pdf", "size": 0, "description": "product data sheet"}
]


Expand Down Expand Up @@ -353,7 +353,7 @@ def test_upload_new_product_document_to_replace_existing_one(
assert document_delete_request.matcher.called_once

assert requests_mock.request_history.pop().json() == [
{"name": "data sheet", "s3_key": "data sheet", "size": 0, "description": "product data sheet"}
{"name": "data_sheet.pdf", "s3_key": "data_sheet.pdf", "size": 0, "description": "product data sheet"}
]

assert requests_mock.request_history.pop().json() == {"is_document_sensitive": False}
Expand Down Expand Up @@ -443,7 +443,7 @@ def test_edit_product_document_availability_upload_new_document(
assert document_delete_request.matcher.called_once

assert requests_mock.request_history.pop().json() == [
{"name": "data sheet", "s3_key": "data sheet", "size": 0, "description": "product data sheet"}
{"name": "data_sheet.pdf", "s3_key": "data_sheet.pdf", "size": 0, "description": "product data sheet"}
]

assert requests_mock.request_history.pop().json() == {
Expand Down
Loading

0 comments on commit ee9e36a

Please sign in to comment.