diff --git a/classquiz/config.py b/classquiz/config.py index 50757af..c3f4392 100644 --- a/classquiz/config.py +++ b/classquiz/config.py @@ -1,7 +1,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. - +import re from functools import lru_cache from redis import asyncio as redis_lib @@ -71,3 +71,5 @@ storage: Storage = Storage( meilisearch = MeiliSearch.Client(settings().meilisearch_url) ALLOWED_TAGS_FOR_QUIZ = ["b", "strong", "i", "em", "small", "mark", "del", "sub", "sup"] + +server_regex = rf"^{re.escape(settings().root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$" diff --git a/classquiz/db/models.py b/classquiz/db/models.py index 628c5d5..f977539 100644 --- a/classquiz/db/models.py +++ b/classquiz/db/models.py @@ -2,6 +2,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. import os +import re import uuid from datetime import datetime from typing import Optional @@ -10,6 +11,7 @@ import ormar from pydantic import BaseModel, Json, validator from enum import Enum from . import metadata, database +from ..config import server_regex class UserAuthTypes(Enum): @@ -150,6 +152,16 @@ class QuizInput(BaseModel): cover_image: str | None background_color: str | None questions: list[QuizQuestion] + background_image: str | None + + @validator("background_image") + def must_come_from_local_cdn(cls, v): + if v is None: + return v + elif bool(re.match(server_regex, v)): + return v + else: + raise ValueError("does not match url scheme") class Quiz(ormar.Model): @@ -164,6 +176,16 @@ class Quiz(ormar.Model): imported_from_kahoot: Optional[bool] = ormar.Boolean(default=False, nullable=True) cover_image: Optional[str] = ormar.Text(nullable=True, unique=False) background_color: str | None = ormar.Text(nullable=True, unique=False) + background_image: str | None = ormar.Text(nullable=True, unique=False) + + @validator("background_image") + def must_come_from_local_cdn(cls, v): + if v is None: + return v + elif bool(re.match(server_regex, v)): + return v + else: + raise ValueError("does not match url scheme") class Meta: tablename = "quiz" @@ -211,6 +233,7 @@ class PlayGame(BaseModel): game_mode: str | None current_question: int = -1 background_color: str | None + background_image: str | None custom_field: str | None diff --git a/classquiz/routers/editor.py b/classquiz/routers/editor.py index 7f1459f..9e1213a 100644 --- a/classquiz/routers/editor.py +++ b/classquiz/routers/editor.py @@ -13,7 +13,7 @@ import bleach from fastapi import APIRouter, File, UploadFile, HTTPException, Depends from pydantic import BaseModel -from classquiz.config import settings, redis, storage, meilisearch, ALLOWED_TAGS_FOR_QUIZ +from classquiz.config import settings, redis, storage, meilisearch, ALLOWED_TAGS_FOR_QUIZ, server_regex from classquiz.db.models import Quiz, QuizInput, User, QuizQuestionType import puremagic from classquiz.auth import get_current_user @@ -143,10 +143,10 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): ) image_id_regex = r"^.{36}--.{36}$" imgur_regex = r"^https://i\.imgur\.com\/.{7}.(jpg|png|gif)$" - server_regex = rf"^{re.escape(settings.root_address)}/api/v1/storage/download/.{{36}}--.{{36}}$" + extract_file_name_re = r"^.*/api/v1/storage/download/(.{36}--.{36})$" images_to_delete = [] - old_quiz_data = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id) + old_quiz_data: Quiz = await Quiz.objects.get_or_none(id=session_data.quiz_id, user_id=session_data.user_id) def mark_image_for_deletion(new: str | None, index: int, old_quiz: Quiz | None): if old_quiz is None: @@ -183,6 +183,9 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): if quiz_input.cover_image == "": quiz_input.cover_image = None + # if quiz_input.background_image is None and old_quiz_data.background_image is not None: + # mark_image_for_deletion(quiz_input.background_image) + if quiz_input.cover_image is not None and not bool(re.match(server_regex, quiz_input.cover_image)): raise HTTPException(status_code=400, detail="image url is not valid") @@ -200,6 +203,7 @@ async def finish_edit(edit_id: str, quiz_input: QuizInput): quiz.questions = quiz_input.dict()["questions"] quiz.cover_image = quiz_input.cover_image quiz.background_color = quiz_input.background_color + quiz.background_image = quiz_input.background_image for image in images_to_delete: if image is not None: try: diff --git a/classquiz/routers/quiz.py b/classquiz/routers/quiz.py index 501e09f..211ad95 100644 --- a/classquiz/routers/quiz.py +++ b/classquiz/routers/quiz.py @@ -126,6 +126,7 @@ async def start_quiz( user_id=user.id, background_color=quiz.background_color, custom_field=custom_field, + background_image=quiz.background_image, ) await redis.set(f"game:{str(game.game_pin)}", game.json(), ex=18000) await redis.set(f"game_pin:{user.id}:{quiz_id}", game_pin, ex=18000) diff --git a/frontend/src/lib/editor/settings-card.svelte b/frontend/src/lib/editor/settings-card.svelte index 9d3d5cd..0b6aeb1 100644 --- a/frontend/src/lib/editor/settings-card.svelte +++ b/frontend/src/lib/editor/settings-card.svelte @@ -14,6 +14,7 @@ const { t } = getLocalization(); let uppyOpen = false; + let bg_uppy_open = false; export let edit_id: string; export let data: EditorData; @@ -38,7 +39,12 @@ /> -