diff --git a/backend/server/adventures/models.py b/backend/server/adventures/models.py index ad52e89..c7f78ca 100644 --- a/backend/server/adventures/models.py +++ b/backend/server/adventures/models.py @@ -1,4 +1,4 @@ -from collections.abc import Collection +from django.core.exceptions import ValidationError import os from typing import Iterable import uuid @@ -10,6 +10,13 @@ from django.contrib.postgres.fields import ArrayField from django.forms import ValidationError from django_resized import ResizedImageField +def validate_file_extension(value): + import os + from django.core.exceptions import ValidationError + ext = os.path.splitext(value.name)[1] # [0] returns path+filename + valid_extensions = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.mp4', '.mov', '.avi', '.mkv', '.mp3', '.wav', '.flac', '.ogg', '.m4a', '.wma', '.aac', '.opus', '.zip', '.rar', '.7z', '.tar', '.gz', '.bz2', '.xz', '.zst', '.lz4', '.lzma', '.lzo', '.z', '.tar.gz', '.tar.bz2', '.tar.xz', '.tar.zst', '.tar.lz4', '.tar.lzma', '.tar.lzo', '.tar.z', 'gpx', 'md', 'pdf'] + if not ext.lower() in valid_extensions: + raise ValidationError('Unsupported file extension.') ADVENTURE_TYPES = [ ('general', 'General 🌍'), @@ -306,7 +313,7 @@ class Attachment(models.Model): id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True) user_id = models.ForeignKey( User, on_delete=models.CASCADE, default=default_user_id) - file = models.FileField(upload_to=PathAndRename('attachments/')) + file = models.FileField(upload_to=PathAndRename('attachments/'),validators=[validate_file_extension]) adventure = models.ForeignKey(Adventure, related_name='attachments', on_delete=models.CASCADE) name = models.CharField(max_length=200, null=True, blank=True) diff --git a/frontend/package.json b/frontend/package.json index ae71362..a2ed840 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "dependencies": { "@lukulent/svelte-umami": "^0.0.3", "@mapbox/togeojson": "^0.16.2", + "dompurify": "^3.2.4", "emoji-picker-element": "^1.26.0", "gsap": "^3.12.7", "marked": "^15.0.4", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index edde452..318a11b 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@mapbox/togeojson': specifier: ^0.16.2 version: 0.16.2 + dompurify: + specifier: ^3.2.4 + version: 3.2.4 emoji-picker-element: specifier: ^1.26.0 version: 1.26.0 @@ -816,6 +819,9 @@ packages: '@types/supercluster@7.1.3': resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@vercel/nft@0.27.2': resolution: {integrity: sha512-7LeioS1yE5hwPpQfD3DdH04tuugKjo5KrJk3yK5kAI3Lh76iSsK/ezoFQfzuT08X3ZASQOd1y9ePjLNI9+TxTQ==} engines: {node: '>=16'} @@ -1093,6 +1099,9 @@ packages: dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dompurify@3.2.4: + resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==} + earcut@2.2.4: resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} @@ -2828,6 +2837,9 @@ snapshots: dependencies: '@types/geojson': 7946.0.14 + '@types/trusted-types@2.0.7': + optional: true + '@vercel/nft@0.27.2': dependencies: '@mapbox/node-pre-gyp': 1.0.11 @@ -3094,6 +3106,10 @@ snapshots: dlv@1.1.3: {} + dompurify@3.2.4: + optionalDependencies: + '@types/trusted-types': 2.0.7 + earcut@2.2.4: {} eastasianwidth@0.2.0: {} diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 96648d7..c8b0743 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -14,6 +14,57 @@ let categories: Category[] = []; + const allowedFileTypes = [ + '.pdf', + '.doc', + '.docx', + '.xls', + '.xlsx', + '.ppt', + '.pptx', + '.txt', + '.png', + '.jpg', + '.jpeg', + '.gif', + '.webp', + '.mp4', + '.mov', + '.avi', + '.mkv', + '.mp3', + '.wav', + '.flac', + '.ogg', + '.m4a', + '.wma', + '.aac', + '.opus', + '.zip', + '.rar', + '.7z', + '.tar', + '.gz', + '.bz2', + '.xz', + '.zst', + '.lz4', + '.lzma', + '.lzo', + '.z', + '.tar.gz', + '.tar.bz2', + '.tar.xz', + '.tar.zst', + '.tar.lz4', + '.tar.lzma', + '.tar.lzo', + '.tar.z', + 'gpx', + 'md', + 'pdf' + ]; + export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal let fileInput: HTMLInputElement; @@ -783,7 +834,7 @@ type="file" id="fileInput" class="file-input file-input-bordered w-full max-w-xs" - accept="image/*,video/*,audio/*,application/pdf,.gpx" + accept={allowedFileTypes.join(',')} on:change={handleFileChange} /> diff --git a/frontend/src/lib/components/MarkdownEditor.svelte b/frontend/src/lib/components/MarkdownEditor.svelte index a280d7e..1cfa649 100644 --- a/frontend/src/lib/components/MarkdownEditor.svelte +++ b/frontend/src/lib/components/MarkdownEditor.svelte @@ -1,6 +1,7 @@