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, PageView 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("") if insert_at != -1: button_html = """ """ body = body[:insert_at] + button_html + body[insert_at:] response.set_data(body) return response @slides_bp.route("///") @slides_bp.route("///") @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']}" ) try: pv = PageView(deck_id=deck_info["deck_id"], token_id=token.id) db.session.add(pv) db.session.commit() except Exception as e: db.session.rollback() current_app.logger.error(f"Failed to log page view: {e}") return send_from_directory(deck_path, path) log_404("File missing within deck", deck, course_folder, path, token) abort(404) @slides_bp.route("///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)