From 690c486bb213d4a8832a8a4cece15b50874b4785 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 4 Jul 2022 14:39:53 +0200 Subject: [PATCH] zip files before download in file broswer needs to be completly rewritten in the future but for now this is more secure --- cookbook/serializer.py | 35 +++- cookbook/urls.py | 1 + cookbook/views/api.py | 26 +++ vue/src/components/StepComponent.vue | 6 +- vue/src/utils/openapi/api.ts | 257 ++++++++++++++++++++------- 5 files changed, 255 insertions(+), 70 deletions(-) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index e5b5c396..1e386c5d 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -1,9 +1,11 @@ +import traceback from datetime import timedelta, datetime from decimal import Decimal from gettext import gettext as _ from html import escape from smtplib import SMTPException +from PIL import Image from django.contrib.auth.models import User, Group from django.core.mail import send_mail from django.db.models import Avg, Q, QuerySet, Sum @@ -266,6 +268,20 @@ class UserPreferenceSerializer(WritableNestedModelSerializer): class UserFileSerializer(serializers.ModelSerializer): + file = serializers.FileField(write_only=True) + file_download = serializers.SerializerMethodField('get_download_link') + preview = serializers.SerializerMethodField('get_preview_link') + + def get_download_link(self, obj): + return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk})) + + def get_preview_link(self, obj): + try: + img = Image.open(obj.file.file.file) + return self.context['request'].build_absolute_uri(obj.file.url) + except Exception: + traceback.print_exc() + return "" def check_file_limit(self, validated_data): if 'file' in validated_data: @@ -295,12 +311,25 @@ class UserFileSerializer(serializers.ModelSerializer): class Meta: model = UserFile - fields = ('name', 'file', 'file_size_kb', 'id',) + fields = ('id', 'name', 'file', 'file_download', 'preview', 'file_size_kb') read_only_fields = ('id', 'file_size_kb') extra_kwargs = {"file": {"required": False, }} class UserFileViewSerializer(serializers.ModelSerializer): + file_download = serializers.SerializerMethodField('get_download_link') + preview = serializers.SerializerMethodField('get_preview_link') + + def get_download_link(self, obj): + return self.context['request'].build_absolute_uri(reverse('api_download_file', args={obj.pk})) + + def get_preview_link(self, obj): + try: + img = Image.open(obj.file.file.file) + return self.context['request'].build_absolute_uri(obj.file.url) + except Exception: + traceback.print_exc() + return "" def create(self, validated_data): raise ValidationError('Cannot create File over this view') @@ -310,7 +339,7 @@ class UserFileViewSerializer(serializers.ModelSerializer): class Meta: model = UserFile - fields = ('name', 'file', 'id',) + fields = ('id', 'name', 'file_download', 'preview') read_only_fields = ('id', 'file') @@ -708,7 +737,7 @@ class RecipeSerializer(RecipeBaseSerializer): fields = ( 'id', 'name', 'description', 'image', 'keywords', 'steps', 'working_time', 'waiting_time', 'created_by', 'created_at', 'updated_at', 'source_url', - 'internal', 'show_ingredient_overview','nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked', + 'internal', 'show_ingredient_overview', 'nutrition', 'servings', 'file_path', 'servings_text', 'rating', 'last_cooked', ) read_only_fields = ['image', 'created_by', 'created_at'] diff --git a/cookbook/urls.py b/cookbook/urls.py index 5933787c..2c0acd29 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -118,6 +118,7 @@ urlpatterns = [ path('api/get_facets/', api.get_facets, name='api_get_facets'), path('api/reset-food-inheritance/', api.reset_food_inheritance, name='api_reset_food_inheritance'), path('api/switch-active-space//', api.switch_active_space, name='api_switch_active_space'), + path('api/download-file//', api.download_file, name='api_download_file'), path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'), diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 2443bf3c..7a49261b 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -5,6 +5,7 @@ import re import traceback import uuid from collections import OrderedDict +from zipfile import ZipFile import requests import validators @@ -1216,6 +1217,31 @@ def switch_active_space(request, space_id): return Response({}, status=status.HTTP_400_BAD_REQUEST) +@api_view(['GET']) +# @schema(AutoSchema()) #TODO add proper schema +@permission_classes([CustomIsUser]) +def download_file(request, file_id): + """ + function to download a user file securely (wrapping as zip to prevent any context based XSS problems) + temporary solution until a real file manager is implemented + """ + try: + uf = UserFile.objects.get(space=request.space, pk=file_id) + + in_memory = io.BytesIO() + zf = ZipFile(in_memory, mode="w") + zf.writestr(uf.file.name, uf.file.file.read()) + zf.close() + + response = HttpResponse(in_memory.getvalue(), content_type='application/force-download') + response['Content-Disposition'] = 'attachment; filename="' + uf.name + '.zip"' + return response + + except Exception as e: + traceback.print_exc() + return Response({}, status=status.HTTP_400_BAD_REQUEST) + + def get_recipe_provider(recipe): if recipe.storage.method == Storage.DROPBOX: return Dropbox diff --git a/vue/src/components/StepComponent.vue b/vue/src/components/StepComponent.vue index f7c2b8ba..fe1acf90 100644 --- a/vue/src/components/StepComponent.vue +++ b/vue/src/components/StepComponent.vue @@ -55,11 +55,11 @@