mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-23 06:49:37 +02:00
feat: Add file type validation and sanitize markdown input in adventure components
This commit is contained in:
parent
50a732b4d7
commit
7fbcf170d0
6 changed files with 84 additions and 8 deletions
|
@ -1,4 +1,4 @@
|
||||||
from collections.abc import Collection
|
from django.core.exceptions import ValidationError
|
||||||
import os
|
import os
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
import uuid
|
import uuid
|
||||||
|
@ -10,6 +10,13 @@ from django.contrib.postgres.fields import ArrayField
|
||||||
from django.forms import ValidationError
|
from django.forms import ValidationError
|
||||||
from django_resized import ResizedImageField
|
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 = [
|
ADVENTURE_TYPES = [
|
||||||
('general', 'General 🌍'),
|
('general', 'General 🌍'),
|
||||||
|
@ -306,7 +313,7 @@ class Attachment(models.Model):
|
||||||
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True, primary_key=True)
|
||||||
user_id = models.ForeignKey(
|
user_id = models.ForeignKey(
|
||||||
User, on_delete=models.CASCADE, default=default_user_id)
|
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)
|
adventure = models.ForeignKey(Adventure, related_name='attachments', on_delete=models.CASCADE)
|
||||||
name = models.CharField(max_length=200, null=True, blank=True)
|
name = models.CharField(max_length=200, null=True, blank=True)
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lukulent/svelte-umami": "^0.0.3",
|
"@lukulent/svelte-umami": "^0.0.3",
|
||||||
"@mapbox/togeojson": "^0.16.2",
|
"@mapbox/togeojson": "^0.16.2",
|
||||||
|
"dompurify": "^3.2.4",
|
||||||
"emoji-picker-element": "^1.26.0",
|
"emoji-picker-element": "^1.26.0",
|
||||||
"gsap": "^3.12.7",
|
"gsap": "^3.12.7",
|
||||||
"marked": "^15.0.4",
|
"marked": "^15.0.4",
|
||||||
|
|
16
frontend/pnpm-lock.yaml
generated
16
frontend/pnpm-lock.yaml
generated
|
@ -14,6 +14,9 @@ importers:
|
||||||
'@mapbox/togeojson':
|
'@mapbox/togeojson':
|
||||||
specifier: ^0.16.2
|
specifier: ^0.16.2
|
||||||
version: 0.16.2
|
version: 0.16.2
|
||||||
|
dompurify:
|
||||||
|
specifier: ^3.2.4
|
||||||
|
version: 3.2.4
|
||||||
emoji-picker-element:
|
emoji-picker-element:
|
||||||
specifier: ^1.26.0
|
specifier: ^1.26.0
|
||||||
version: 1.26.0
|
version: 1.26.0
|
||||||
|
@ -816,6 +819,9 @@ packages:
|
||||||
'@types/supercluster@7.1.3':
|
'@types/supercluster@7.1.3':
|
||||||
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
|
resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==}
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==}
|
||||||
|
|
||||||
'@vercel/nft@0.27.2':
|
'@vercel/nft@0.27.2':
|
||||||
resolution: {integrity: sha512-7LeioS1yE5hwPpQfD3DdH04tuugKjo5KrJk3yK5kAI3Lh76iSsK/ezoFQfzuT08X3ZASQOd1y9ePjLNI9+TxTQ==}
|
resolution: {integrity: sha512-7LeioS1yE5hwPpQfD3DdH04tuugKjo5KrJk3yK5kAI3Lh76iSsK/ezoFQfzuT08X3ZASQOd1y9ePjLNI9+TxTQ==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
@ -1093,6 +1099,9 @@ packages:
|
||||||
dlv@1.1.3:
|
dlv@1.1.3:
|
||||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||||
|
|
||||||
|
dompurify@3.2.4:
|
||||||
|
resolution: {integrity: sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==}
|
||||||
|
|
||||||
earcut@2.2.4:
|
earcut@2.2.4:
|
||||||
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
|
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
|
||||||
|
|
||||||
|
@ -2828,6 +2837,9 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/geojson': 7946.0.14
|
'@types/geojson': 7946.0.14
|
||||||
|
|
||||||
|
'@types/trusted-types@2.0.7':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@vercel/nft@0.27.2':
|
'@vercel/nft@0.27.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mapbox/node-pre-gyp': 1.0.11
|
'@mapbox/node-pre-gyp': 1.0.11
|
||||||
|
@ -3094,6 +3106,10 @@ snapshots:
|
||||||
|
|
||||||
dlv@1.1.3: {}
|
dlv@1.1.3: {}
|
||||||
|
|
||||||
|
dompurify@3.2.4:
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/trusted-types': 2.0.7
|
||||||
|
|
||||||
earcut@2.2.4: {}
|
earcut@2.2.4: {}
|
||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
|
@ -14,6 +14,57 @@
|
||||||
|
|
||||||
let categories: Category[] = [];
|
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
|
export let initialLatLng: { lat: number; lng: number } | null = null; // Used to pass the location from the map selection to the modal
|
||||||
|
|
||||||
let fileInput: HTMLInputElement;
|
let fileInput: HTMLInputElement;
|
||||||
|
@ -783,7 +834,7 @@
|
||||||
type="file"
|
type="file"
|
||||||
id="fileInput"
|
id="fileInput"
|
||||||
class="file-input file-input-bordered w-full max-w-xs"
|
class="file-input file-input-bordered w-full max-w-xs"
|
||||||
accept="image/*,video/*,audio/*,application/pdf,.gpx"
|
accept={allowedFileTypes.join(',')}
|
||||||
on:change={handleFileChange}
|
on:change={handleFileChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { marked } from 'marked'; // Import the markdown parser
|
import { marked } from 'marked'; // Import the markdown parser
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import DOMPurify from 'dompurify'; // Import DOMPurify to sanitize HTML
|
||||||
|
|
||||||
export let text: string | null | undefined = ''; // Markdown text
|
export let text: string | null | undefined = ''; // Markdown text
|
||||||
export let editor_height: string = 'h-64'; // Editor height
|
export let editor_height: string = 'h-64'; // Editor height
|
||||||
|
@ -8,7 +9,7 @@
|
||||||
|
|
||||||
// Function to parse markdown to HTML
|
// Function to parse markdown to HTML
|
||||||
const renderMarkdown = (markdown: string) => {
|
const renderMarkdown = (markdown: string) => {
|
||||||
return marked(markdown);
|
return marked(markdown) as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// References for scroll syncing
|
// References for scroll syncing
|
||||||
|
@ -61,7 +62,7 @@
|
||||||
class="prose overflow-auto h-96 max-w-full w-full p-4 border border-base-300 rounded-lg bg-base-300"
|
class="prose overflow-auto h-96 max-w-full w-full p-4 border border-base-300 rounded-lg bg-base-300"
|
||||||
bind:this={previewRef}
|
bind:this={previewRef}
|
||||||
>
|
>
|
||||||
{@html renderMarkdown(text || '')}
|
{@html DOMPurify.sanitize(renderMarkdown(text || ''))}
|
||||||
</article>
|
</article>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import { DefaultMarker, MapLibre, Popup, GeoJSON, LineLayer } from 'svelte-maplibre';
|
import { DefaultMarker, MapLibre, Popup, GeoJSON, LineLayer } from 'svelte-maplibre';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { marked } from 'marked'; // Import the markdown parser
|
import { marked } from 'marked'; // Import the markdown parser
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import toGeoJSON from '@mapbox/togeojson';
|
import toGeoJSON from '@mapbox/togeojson';
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
let geojson: any;
|
let geojson: any;
|
||||||
|
|
||||||
const renderMarkdown = (markdown: string) => {
|
const renderMarkdown = (markdown: string) => {
|
||||||
return marked(markdown);
|
return marked(markdown) as string;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getGpxFiles() {
|
async function getGpxFiles() {
|
||||||
|
@ -369,7 +369,7 @@
|
||||||
<article
|
<article
|
||||||
class="prose overflow-auto h-full max-w-full p-4 border border-base-300 rounded-lg"
|
class="prose overflow-auto h-full max-w-full p-4 border border-base-300 rounded-lg"
|
||||||
>
|
>
|
||||||
{@html renderMarkdown(adventure.description)}
|
{@html DOMPurify.sanitize(renderMarkdown(adventure.description))}
|
||||||
</article>
|
</article>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue