""" Утилиты для работы с домашними заданиями. """ import re import os import logging from django.core.exceptions import ValidationError logger = logging.getLogger(__name__) # Теги, разрешённые после конвертации Markdown + MathML (для санитизации) ALLOWED_FEEDBACK_HTML_TAGS = [ 'p', 'br', 'div', 'span', 'strong', 'em', 'b', 'i', 'u', 's', 'ul', 'ol', 'li', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'a', 'sub', 'sup', # MathML (для формул из LaTeX) 'math', 'mrow', 'mi', 'mo', 'mn', 'msup', 'msub', 'munder', 'mover', 'mfrac', 'mtable', 'mtr', 'mtd', 'mstyle', 'mtext', 'annotation', 'semantics', ] def feedback_to_html(raw: str) -> str: """ Переводит ответ ИИ (markdown + LaTeX вроде $a$, $$x^2$$) в безопасный HTML. Используется для отображения ai_feedback и комментариев проверки без «сырых» символов $. """ if not raw or not raw.strip(): return '' try: import markdown import bleach except ImportError: logger.warning("markdown/bleach не установлены — возвращаем экранированный текст") return _escape_html(raw) # 1) Выносим LaTeX в placeholder'ы, чтобы markdown их не трогал block_maths = [] inline_maths = [] def block_replacer(m): block_maths.append(m.group(1).strip()) return f'\x00MATH_BLOCK_{len(block_maths) - 1}\x00' def inline_replacer(m): inline_maths.append(m.group(1).strip()) return f'\x00MATH_INLINE_{len(inline_maths) - 1}\x00' text = raw # Сначала блочные $$ ... $$ text = re.sub(r'\$\$([^$]*?)\$\$', block_replacer, text, flags=re.DOTALL) # Потом инлайновые $ ... $ (не захватываем переносы в инлайне) text = re.sub(r'\$([^$\n]+?)\$', inline_replacer, text) # 2) Markdown → HTML html = markdown.markdown( text, extensions=['nl2br'], output_format='html5', ) # 3) LaTeX → MathML и подставляем обратно try: from latex2mathml.converter import convert as latex_to_mathml except ImportError: latex_to_mathml = None def replace_math(placeholder_prefix, maths_list): for i, latex in enumerate(maths_list): token = f'\x00{placeholder_prefix}_{i}\x00' if latex_to_mathml and latex: try: mathml = latex_to_mathml(latex) # latex2mathml возвращает полный ... html_replacement = f'{mathml}' except Exception as e: logger.debug("LaTeX → MathML failed for %r: %s", latex[:50], e) html_replacement = _escape_html(f'${latex}$') else: html_replacement = _escape_html(f'${latex}$') nonlocal html html = html.replace(token, html_replacement) replace_math('MATH_BLOCK', block_maths) replace_math('MATH_INLINE', inline_maths) # 4) Санитизация (разрешаем class, xmlns для формул и ссылок href) allowed_attrs = {'*': ['class'], 'a': ['href', 'title', 'rel'], 'math': ['xmlns'], 'span': ['class']} html = bleach.clean(html, tags=ALLOWED_FEEDBACK_HTML_TAGS, attributes=allowed_attrs, strip=True) return html.strip() def _escape_html(s: str) -> str: """Экранирует HTML-сущности.""" return ( s.replace('&', '&') .replace('<', '<') .replace('>', '>') .replace('"', '"') ) def sanitize_filename(filename): """ Очистка имени файла от опасных символов. Args: filename: Исходное имя файла Returns: str: Безопасное имя файла """ # Удаляем путь, оставляем только имя файла filename = os.path.basename(filename) # Удаляем опасные символы filename = re.sub(r'[<>:"/\\|?*\x00-\x1f]', '', filename) # Удаляем ведущие/завершающие точки и пробелы filename = filename.strip('. ') # Ограничиваем длину if len(filename) > 255: name, ext = os.path.splitext(filename) filename = name[:250] + ext # Если имя пустое, возвращаем дефолтное if not filename: filename = 'file' return filename def validate_file_type(filename, allowed_types): """ Проверка типа файла. Args: filename: Имя файла allowed_types: Строка с разрешенными расширениями (например, ".pdf,.doc,.docx") Returns: bool: True если тип разрешен """ if not allowed_types: return True # Получаем расширение файла ext = os.path.splitext(filename)[1].lower() # Нормализуем allowed_types allowed_list = [t.strip().lower() for t in allowed_types.split(',') if t.strip()] return ext in allowed_list def validate_file_size(file_size, max_size): """ Проверка размера файла. Args: file_size: Размер файла в байтах max_size: Максимальный размер в байтах Returns: bool: True если размер допустим """ if max_size <= 0: return True return file_size <= max_size