159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
from . import slides_bp
|
|
from flask import current_app, abort, redirect, url_for, request, send_from_directory
|
|
from app.utils import get_cached_deck_info, token_required
|
|
from app.models import Deck, db, Course
|
|
import os
|
|
import flask_login
|
|
import re
|
|
|
|
|
|
slidev_deck_pattern = re.compile(
|
|
r"^\/slides\/[^\/]+\/[^\/]+\/(?:\d+|index\.html)(?:\?.*)?$"
|
|
)
|
|
slidev_all_pattern = re.compile(
|
|
r"^\/slides\/([^\/]+)\/([^\/]+)(?:\/presenter)?\/(\d+)(?:\?.*)?$"
|
|
)
|
|
|
|
|
|
def log_404(reason: str, deck: str, course: str, path: str, token):
|
|
"""
|
|
Helper function to log 404 errors with the user's identity.
|
|
Uses current_app.logger to ensure logs are handled by the app's config.
|
|
"""
|
|
user_identity = f"User: {token.name} (ID: {token.id})"
|
|
current_app.logger.warning(
|
|
f"404 Not Found [{reason}] | {user_identity} | Resource: {course}/{deck}/{path}"
|
|
)
|
|
|
|
|
|
@slides_bp.after_request
|
|
def inject_confused_button(response):
|
|
if (
|
|
(slidev_deck_pattern.match(request.path) or request.path == "/")
|
|
and response.content_type.startswith("text/html")
|
|
and not flask_login.current_user.is_authenticated
|
|
):
|
|
if response.direct_passthrough:
|
|
response.direct_passthrough = False
|
|
body = response.get_data(as_text=True)
|
|
insert_at = body.find("</body>")
|
|
if insert_at != -1:
|
|
button_html = """
|
|
<button id="confused-button" style="
|
|
position: fixed;
|
|
bottom: 1.5rem;
|
|
right: 1.5rem;
|
|
z-index: 9999;
|
|
background-color: #4f8cff;
|
|
color: white;
|
|
font-size: 1rem;
|
|
font-weight: bold;
|
|
border: none;
|
|
border-radius: 9999px;
|
|
padding: 1rem;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
cursor: pointer;
|
|
transition: background-color 0.3s, transform 0.2s;"
|
|
title="Didn't understand? Click!">
|
|
Ich habe das nicht verstanden
|
|
</button>
|
|
<script>
|
|
document.getElementById('confused-button').addEventListener('click', () => {
|
|
fetch('/confused', {method: 'POST'})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
alert('Alles klar! Ich versuche es besser zu erklären. Melde dich, wenn du eine spezifische Frage hast oder noch etwas unklar bleibt.');
|
|
}
|
|
}).catch(error => console.error('Error:', error));
|
|
});
|
|
</script>
|
|
"""
|
|
body = body[:insert_at] + button_html + body[insert_at:]
|
|
response.set_data(body)
|
|
return response
|
|
|
|
|
|
@slides_bp.route("/<course_folder>/<deck>/")
|
|
@slides_bp.route("/<course_folder>/<deck>/<path:path>")
|
|
@token_required
|
|
def serve_deck(course_folder, deck, token, path="index.html"):
|
|
# 1. Ask the cache for the deck data (0 DB queries on a cache hit!)
|
|
deck_info = get_cached_deck_info(token.id, course_folder, deck)
|
|
|
|
# 2. Handle missing data
|
|
if not deck_info["course_exists"]:
|
|
log_404("Course not found", deck, course_folder, path, token)
|
|
abort(404)
|
|
|
|
if not deck_info["deck_exists"]:
|
|
log_404("Deck not in database", deck, course_folder, path, token)
|
|
abort(404)
|
|
|
|
# 3. Handle authorization
|
|
if not deck_info["auth"]:
|
|
current_app.logger.warning(
|
|
f"Access denied (locked) for user {token.name} -> {deck_info.get('deck_name', deck)}"
|
|
)
|
|
return redirect(url_for("main.index"))
|
|
|
|
# 4. Resolve the file path entirely using strings
|
|
slides_dir = os.path.join(current_app.config["SLIDES_DIR"], course_folder)
|
|
deck_path = os.path.join(slides_dir, deck)
|
|
|
|
if not os.path.exists(deck_path):
|
|
log_404("Deck directory missing on disk", deck, course_folder, path, token)
|
|
return f"Deck '{deck_info['deck_name']}' not found.", 404
|
|
|
|
# 5. Serve the file
|
|
if slidev_all_pattern.match(request.path):
|
|
current_app.logger.info(f"User reloaded in deck {deck_info['deck_name']}")
|
|
return send_from_directory(deck_path, deck_info["index_file"])
|
|
|
|
requested_file = os.path.join(deck_path, path)
|
|
if os.path.isfile(requested_file):
|
|
if path == deck_info["index_file"]:
|
|
current_app.logger.info(
|
|
f"User {token.name} accessed deck {deck_info['deck_name']}"
|
|
)
|
|
return send_from_directory(deck_path, path)
|
|
|
|
log_404("File missing within deck", deck, course_folder, path, token)
|
|
abort(404)
|
|
|
|
|
|
@slides_bp.route("/<course_folder>/<deck>/export")
|
|
@token_required
|
|
def serve_export(deck, course_folder, token):
|
|
course = db.session.execute(
|
|
db.select(Course).where(Course.folder == course_folder)
|
|
).scalar()
|
|
if not course:
|
|
log_404("Export: Course not found", deck, course_folder, "", token)
|
|
abort(404)
|
|
assert isinstance(course, Course)
|
|
|
|
deck_obj = db.session.execute(
|
|
db.select(Deck).where(Deck.deck == deck, Deck.course_id == course.id)
|
|
).scalar()
|
|
if not deck_obj:
|
|
log_404("Export: Deck not in database", deck, course_folder, "", token)
|
|
abort(404)
|
|
assert isinstance(deck_obj, Deck)
|
|
|
|
if not deck_obj.export_file:
|
|
log_404("Export: Deck has no export_file set", deck, course_folder, "", token)
|
|
abort(404)
|
|
|
|
if not token.is_unlocked(deck_obj.deck):
|
|
return redirect(url_for("main.index"))
|
|
|
|
exports_dir = os.path.join(current_app.config["EXPORTS_DIR"], course.folder)
|
|
deck_path = os.path.join(exports_dir, deck_obj.deck)
|
|
requested_file = os.path.join(deck_path, deck_obj.export_file)
|
|
if os.path.isfile(requested_file):
|
|
current_app.logger.info(f"User {token.name} exported deck {deck_obj.name}")
|
|
return send_from_directory(deck_path, deck_obj.export_file)
|
|
|
|
log_404("Export file missing", deck, course_folder, deck_obj.export_file, token)
|
|
abort(404)
|