From c997da278ffc6ec1c985c070995025fd10b73daa Mon Sep 17 00:00:00 2001 From: rup-narayan-rajbanshi Date: Wed, 4 Sep 2024 11:53:57 +0545 Subject: [PATCH] Add custom SecureFileField class in FileField. --- ...alter_generaldocument_document_and_more.py | 25 ++++++++++++++ api/models.py | 6 ++-- ...08_alter_countryplan_internal_plan_file.py | 21 ++++++++++++ country_plan/models.py | 3 +- ...le_preview_alter_dreffile_file_and_more.py | 34 +++++++++++++++++++ dref/models.py | 9 ++--- .../0013_alter_flashgraphicmap_file.py | 19 ----------- ...013_alter_flashgraphicmap_file_and_more.py | 27 +++++++++++++++ .../0014_alter_flashupdate_extracted_file.py | 19 ----------- flash_update/models.py | 14 +++++--- main/base_class.py | 31 +++++++++++++++++ main/utils.py | 11 +++--- ...rdocumentupload_file_alter_perfile_file.py | 24 +++++++++++++ per/models.py | 5 +-- 14 files changed, 192 insertions(+), 56 deletions(-) create mode 100644 api/migrations/0214_alter_generaldocument_document_and_more.py create mode 100644 country_plan/migrations/0008_alter_countryplan_internal_plan_file.py create mode 100644 dref/migrations/0075_alter_dref_budget_file_preview_alter_dreffile_file_and_more.py delete mode 100644 flash_update/migrations/0013_alter_flashgraphicmap_file.py create mode 100644 flash_update/migrations/0013_alter_flashgraphicmap_file_and_more.py delete mode 100644 flash_update/migrations/0014_alter_flashupdate_extracted_file.py create mode 100644 main/base_class.py create mode 100644 per/migrations/0121_alter_perdocumentupload_file_alter_perfile_file.py diff --git a/api/migrations/0214_alter_generaldocument_document_and_more.py b/api/migrations/0214_alter_generaldocument_document_and_more.py new file mode 100644 index 0000000000..5090866128 --- /dev/null +++ b/api/migrations/0214_alter_generaldocument_document_and_more.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.15 on 2024-09-10 10:12 + +import api.models +from django.db import migrations +import main.base_class + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0213_merge_20240807_1001'), + ] + + operations = [ + migrations.AlterField( + model_name='generaldocument', + name='document', + field=main.base_class.SecureFileField(blank=True, null=True, upload_to=api.models.general_document_path, verbose_name='document'), + ), + migrations.AlterField( + model_name='situationreport', + name='document', + field=main.base_class.SecureFileField(blank=True, null=True, upload_to=api.models.sitrep_document_path, verbose_name='document'), + ), + ] diff --git a/api/models.py b/api/models.py index 74bfe44d79..8bcaf2e8e2 100644 --- a/api/models.py +++ b/api/models.py @@ -22,6 +22,8 @@ from django.utils.translation import gettext_lazy as _ from tinymce.models import HTMLField +from main.base_class import SecureFileField + from .utils import validate_slug_number # is_user_ifrc, @@ -984,7 +986,7 @@ def sitrep_document_path(instance, filename): class SituationReport(models.Model): created_at = models.DateTimeField(verbose_name=_("created at"), auto_now_add=True) name = models.CharField(verbose_name=_("name"), max_length=100) - document = models.FileField(verbose_name=_("document"), null=True, blank=True, upload_to=sitrep_document_path) + document = SecureFileField(verbose_name=_("document"), null=True, blank=True, upload_to=sitrep_document_path) document_url = models.URLField(verbose_name=_("document url"), blank=True) event = models.ForeignKey(Event, verbose_name=_("event"), on_delete=models.CASCADE) @@ -1287,7 +1289,7 @@ class GeneralDocument(models.Model): name = models.CharField(verbose_name=_("name"), max_length=100) # Don't set `auto_now_add` so we can modify it on save created_at = models.DateTimeField(verbose_name=_("created at"), blank=True) - document = models.FileField(verbose_name=_("document"), null=True, blank=True, upload_to=general_document_path) + document = SecureFileField(verbose_name=_("document"), null=True, blank=True, upload_to=general_document_path) document_url = models.URLField(verbose_name=_("document url"), blank=True) class Meta: diff --git a/country_plan/migrations/0008_alter_countryplan_internal_plan_file.py b/country_plan/migrations/0008_alter_countryplan_internal_plan_file.py new file mode 100644 index 0000000000..d39b49be73 --- /dev/null +++ b/country_plan/migrations/0008_alter_countryplan_internal_plan_file.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.15 on 2024-09-10 10:12 + +import country_plan.models +import django.core.validators +from django.db import migrations +import main.base_class + + +class Migration(migrations.Migration): + + dependencies = [ + ('country_plan', '0007_alter_membershipcoordination_sector_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='countryplan', + name='internal_plan_file', + field=main.base_class.SecureFileField(blank=True, null=True, upload_to=country_plan.models.pdf_upload_to, validators=[django.core.validators.FileExtensionValidator(['pdf'])], verbose_name='Internal Plan'), + ), + ] diff --git a/country_plan/models.py b/country_plan/models.py index 56dd056872..f269e14f13 100644 --- a/country_plan/models.py +++ b/country_plan/models.py @@ -5,6 +5,7 @@ from django.utils.translation import gettext_lazy as _ from api.models import Country +from main.base_class import SecureFileField def file_upload_to(instance, filename): @@ -63,7 +64,7 @@ def save(self, *args, **kwargs): class CountryPlan(CountryPlanAbstract): country = models.OneToOneField(Country, on_delete=models.CASCADE, related_name="country_plan", primary_key=True) - internal_plan_file = models.FileField( + internal_plan_file = SecureFileField( verbose_name=_("Internal Plan"), upload_to=pdf_upload_to, validators=[FileExtensionValidator(["pdf"])], diff --git a/dref/migrations/0075_alter_dref_budget_file_preview_alter_dreffile_file_and_more.py b/dref/migrations/0075_alter_dref_budget_file_preview_alter_dreffile_file_and_more.py new file mode 100644 index 0000000000..4be2c8837e --- /dev/null +++ b/dref/migrations/0075_alter_dref_budget_file_preview_alter_dreffile_file_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.2.15 on 2024-09-10 10:12 + +from django.db import migrations +import main.base_class + + +class Migration(migrations.Migration): + + dependencies = [ + ('dref', '0074_auto_20240129_0909'), + ] + + operations = [ + migrations.AlterField( + model_name='dref', + name='budget_file_preview', + field=main.base_class.SecureFileField(blank=True, null=True, upload_to='dref/images/', verbose_name='budget file preview'), + ), + migrations.AlterField( + model_name='dreffile', + name='file', + field=main.base_class.SecureFileField(upload_to='dref/images/', verbose_name='file'), + ), + migrations.AlterField( + model_name='dreffinalreport', + name='financial_report_preview', + field=main.base_class.SecureFileField(blank=True, null=True, upload_to='dref/images/', verbose_name='financial preview'), + ), + migrations.AlterField( + model_name='drefoperationalupdate', + name='budget_file_preview', + field=main.base_class.SecureFileField(blank=True, null=True, upload_to='dref-op-update/images/', verbose_name='budget file preview'), + ), + ] diff --git a/dref/models.py b/dref/models.py index 11b5dd24b9..726342ecf9 100644 --- a/dref/models.py +++ b/dref/models.py @@ -12,6 +12,7 @@ from pdf2image import convert_from_bytes from api.models import Country, DisasterType, District, FieldReport +from main.base_class import SecureFileField @reversion.register() @@ -536,7 +537,7 @@ class Status(models.IntegerChoices): verbose_name=_("budget file"), related_name="budget_file_dref", ) - budget_file_preview = models.FileField(verbose_name=_("budget file preview"), null=True, blank=True, upload_to="dref/images/") + budget_file_preview = SecureFileField(verbose_name=_("budget file preview"), null=True, blank=True, upload_to="dref/images/") assessment_report = models.ForeignKey( "DrefFile", on_delete=models.SET_NULL, @@ -648,7 +649,7 @@ def get_for(user): class DrefFile(models.Model): - file = models.FileField( + file = SecureFileField( verbose_name=_("file"), upload_to="dref/images/", ) @@ -750,7 +751,7 @@ class DrefOperationalUpdate(models.Model): verbose_name=_("budget file"), related_name="budget_file_dref_operational_update", ) - budget_file_preview = models.FileField( + budget_file_preview = SecureFileField( verbose_name=_("budget file preview"), null=True, blank=True, upload_to="dref-op-update/images/" ) assessment_report = models.ForeignKey( @@ -1316,7 +1317,7 @@ class DrefFinalReport(models.Model): verbose_name=_("financial report"), related_name="financial_report_dref_final_report", ) - financial_report_preview = models.FileField( + financial_report_preview = SecureFileField( verbose_name=_("financial preview"), null=True, blank=True, upload_to="dref/images/" ) num_assisted = models.IntegerField(verbose_name=_("number of assisted"), blank=True, null=True) diff --git a/flash_update/migrations/0013_alter_flashgraphicmap_file.py b/flash_update/migrations/0013_alter_flashgraphicmap_file.py deleted file mode 100644 index 586af05b4c..0000000000 --- a/flash_update/migrations/0013_alter_flashgraphicmap_file.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.15 on 2024-09-03 09:37 - -from django.db import migrations, models -import flash_update.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('flash_update', '0012_auto_20230410_0720'), - ] - - operations = [ - migrations.AlterField( - model_name='flashgraphicmap', - name='file', - field=models.FileField(upload_to=flash_update.models.flash_map_upload_to, verbose_name='file'), - ), - ] diff --git a/flash_update/migrations/0013_alter_flashgraphicmap_file_and_more.py b/flash_update/migrations/0013_alter_flashgraphicmap_file_and_more.py new file mode 100644 index 0000000000..0de9f66696 --- /dev/null +++ b/flash_update/migrations/0013_alter_flashgraphicmap_file_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.15 on 2024-09-10 10:12 + +from django.db import migrations + +import main.base_class + + +class Migration(migrations.Migration): + + dependencies = [ + ("flash_update", "0012_auto_20230410_0720"), + ] + + operations = [ + migrations.AlterField( + model_name="flashgraphicmap", + name="file", + field=main.base_class.SecureFileField(upload_to="flash_update/images", verbose_name="file"), + ), + migrations.AlterField( + model_name="flashupdate", + name="extracted_file", + field=main.base_class.SecureFileField( + blank=True, null=True, upload_to="flash_update/pdf/", verbose_name="extracted file" + ), + ), + ] diff --git a/flash_update/migrations/0014_alter_flashupdate_extracted_file.py b/flash_update/migrations/0014_alter_flashupdate_extracted_file.py deleted file mode 100644 index 08a63456d8..0000000000 --- a/flash_update/migrations/0014_alter_flashupdate_extracted_file.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.15 on 2024-09-03 09:42 - -from django.db import migrations, models -import flash_update.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('flash_update', '0013_alter_flashgraphicmap_file'), - ] - - operations = [ - migrations.AlterField( - model_name='flashupdate', - name='extracted_file', - field=models.FileField(blank=True, null=True, upload_to=flash_update.models.flash_extracted_file_upload_to, verbose_name='extracted file'), - ), - ] diff --git a/flash_update/models.py b/flash_update/models.py index 53a70ef04e..09311ed1a4 100644 --- a/flash_update/models.py +++ b/flash_update/models.py @@ -8,7 +8,6 @@ from django.utils.translation import gettext_lazy as _ from tinymce.models import HTMLField -from main.utils import custom_upload_to from api.models import ( ActionCategory, ActionOrg, @@ -17,16 +16,21 @@ DisasterType, District, ) +from main.base_class import SecureFileField +from main.utils import custom_upload_to + def flash_map_upload_to(instance, filename): - return custom_upload_to('flash_update/images/')(instance, filename) + return custom_upload_to("flash_update/images/")(instance, filename) + def flash_extracted_file_upload_to(instance, filename): - return custom_upload_to('flash_update/pdf/')(instance, filename) + return custom_upload_to("flash_update/pdf/")(instance, filename) + @reversion.register() class FlashGraphicMap(models.Model): - file = models.FileField(verbose_name=_("file"), upload_to=flash_map_upload_to) + file = SecureFileField(verbose_name=_("file"), upload_to="flash_update/images") caption = models.CharField(max_length=225, blank=True, null=True) created_by = models.ForeignKey( settings.AUTH_USER_MODEL, @@ -124,7 +128,7 @@ class FlashShareWith(models.TextChoices): verbose_name=_("share with"), ) references = models.ManyToManyField(FlashReferences, blank=True, verbose_name=_("references")) - extracted_file = models.FileField(verbose_name=_("extracted file"), upload_to=flash_extracted_file_upload_to, blank=True, null=True) + extracted_file = SecureFileField(verbose_name=_("extracted file"), upload_to="flash_update/pdf/", blank=True, null=True) extracted_at = models.DateTimeField(verbose_name=_("extracted at"), blank=True, null=True) class Meta: diff --git a/main/base_class.py b/main/base_class.py new file mode 100644 index 0000000000..2b48fad67b --- /dev/null +++ b/main/base_class.py @@ -0,0 +1,31 @@ +import os +import datetime +import posixpath +from uuid import uuid4 + +from django.core.files.utils import validate_file_name +from django.db.models.fields.files import FileField + + +class SecureFileField(FileField): + def generate_filename(self, instance, filename): + """ + Apply (if callable) or prepend (if a string) upload_to to the filename, + then delegate further processing of the name to the storage backend. + Until the storage layer, all file paths are expected to be Unix style + (with forward slashes). + """ + extension = filename.split(".")[-1] + old_file_name = filename.split(".")[0] + # Create a unique filename using uuid4 + filename = f"{old_file_name}-{uuid4().hex}.{extension}" + + if callable(self.upload_to): + filename = self.upload_to(instance, filename) + else: + dirname = datetime.datetime.now().strftime(str(self.upload_to)) + filename = posixpath.join(dirname, filename) + filename = validate_file_name(filename, allow_relative_path=True) + + return self.storage.generate_filename(filename) + diff --git a/main/utils.py b/main/utils.py index f51b0c2204..f18a44b7d6 100644 --- a/main/utils.py +++ b/main/utils.py @@ -1,10 +1,10 @@ -import os import datetime import json +import os import typing -from uuid import uuid4 from collections import defaultdict from tempfile import NamedTemporaryFile, _TemporaryFileWrapper +from uuid import uuid4 import requests from django.conf import settings @@ -21,16 +21,19 @@ def custom_upload_to(directory): """ Rename file name with adding uuid """ + def upload_to(instance, filename): # Get the file extension - extension = filename.split('.')[-1] - old_file_name = filename.split('.')[0] + extension = filename.split(".")[-1] + old_file_name = filename.split(".")[0] # Create a unique filename using uuid4 new_filename = f"{old_file_name}-{uuid4().hex}.{extension}" # Return the new file path return os.path.join(directory, new_filename) + return upload_to + def is_tableau(request): """Checking the request for the 'tableau' parameter (used mostly for switching to the *TableauSerializers) diff --git a/per/migrations/0121_alter_perdocumentupload_file_alter_perfile_file.py b/per/migrations/0121_alter_perdocumentupload_file_alter_perfile_file.py new file mode 100644 index 0000000000..3a2c7816d2 --- /dev/null +++ b/per/migrations/0121_alter_perdocumentupload_file_alter_perfile_file.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.15 on 2024-09-10 10:12 + +from django.db import migrations +import main.base_class + + +class Migration(migrations.Migration): + + dependencies = [ + ('per', '0120_alter_formcomponent_status'), + ] + + operations = [ + migrations.AlterField( + model_name='perdocumentupload', + name='file', + field=main.base_class.SecureFileField(upload_to='per/documents/', verbose_name='file'), + ), + migrations.AlterField( + model_name='perfile', + name='file', + field=main.base_class.SecureFileField(upload_to='per/images/', verbose_name='file'), + ), + ] diff --git a/per/models.py b/per/models.py index 0eff54a466..9978ff4d11 100644 --- a/per/models.py +++ b/per/models.py @@ -6,6 +6,7 @@ from api.models import Appeal, Country from deployments.models import SectorTag +from main.base_class import SecureFileField class ProcessPhase(models.IntegerChoices): @@ -417,7 +418,7 @@ def save(self, *args, **kwargs): class PerFile(models.Model): - file = models.FileField( + file = SecureFileField( verbose_name=_("file"), upload_to="per/images/", ) @@ -733,7 +734,7 @@ def save(self, *args, **kwargs): class PerDocumentUpload(models.Model): - file = models.FileField( + file = SecureFileField( verbose_name=_("file"), upload_to="per/documents/", )