fix: harden school imports and add optional feedback diagnostics

This commit is contained in:
Codex
2026-10-15 10:37:32 +08:00
parent 674df1ceb3
commit 7299f4d8b6
25 changed files with 722 additions and 42 deletions
+23 -4
View File
@@ -29,7 +29,8 @@ from starlette.concurrency import run_in_threadpool
MIB = 1024 * 1024
FILE_LIMIT = 8 * MIB
TOTAL_FILE_LIMIT = 20 * MIB
BODY_LIMIT = TOTAL_FILE_LIMIT + 64 * 1024
LOG_LIMIT = 256 * 1024
BODY_LIMIT = TOTAL_FILE_LIMIT + LOG_LIMIT + 64 * 1024
ID_PATTERN = re.compile(r"[A-Za-z0-9_-]{16,128}\Z")
ADDRESS_PATTERN = re.compile(r"[A-Za-z0-9._%+-]+@(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,63}\Z")
@@ -362,10 +363,15 @@ def safe_filename(filename, extension):
async def parse_submission(request):
try:
async with request.form(max_files=3, max_fields=5, max_part_size=20000) as form:
fields, uploads = {}, []
async with request.form(max_files=4, max_fields=5, max_part_size=20000) as form:
fields, uploads, diagnostic_upload = {}, [], None
for name, value in form.multi_items():
if isinstance(value, UploadFile):
if name == "diagnostics":
if diagnostic_upload is not None:
raise ApiError(422, "validation_error", "Only one diagnostic log is allowed.")
diagnostic_upload = value
continue
if name != "attachments":
raise ApiError(422, "validation_error", "Unexpected file field.")
uploads.append(value)
@@ -387,10 +393,12 @@ async def parse_submission(request):
if kind == "problem":
if not fields.get("message", "").strip() or "school" in fields:
raise ApiError(422, "validation_error", "Problem feedback requires message and must omit school.")
elif ("message" in fields or uploads or not fields.get("school", "").strip()
elif ("message" in fields or uploads or diagnostic_upload or not fields.get("school", "").strip()
or not fields.get("contact", "").strip()):
raise ApiError(422, "validation_error", "Development requests require school and contact; omit message and attachments.")
attachments, total = [], 0
if len(uploads) > 3:
raise ApiError(413, "upload_too_large", "Maximum three media files.")
for upload in uploads:
data = await upload.read(FILE_LIMIT + 1)
total += len(data)
@@ -401,6 +409,17 @@ async def parse_submission(request):
if not valid:
raise ApiError(415, "unsupported_media", "Attachment MIME type and file signature must match a supported image or video.")
attachments.append((safe_filename(upload.filename, extension), mime, data))
if diagnostic_upload is not None:
data = await diagnostic_upload.read(LOG_LIMIT + 1)
if len(data) > LOG_LIMIT:
raise ApiError(413, "upload_too_large", "Diagnostic log exceeds 256 KiB.")
try:
text = data.decode("utf-8")
except UnicodeDecodeError:
raise ApiError(415, "unsupported_media", "Diagnostic log must be UTF-8 text.") from None
if (diagnostic_upload.content_type or '').split(';')[0].strip().lower() != 'text/plain' or '\x00' in text or not text.startswith('TPSchedule diagnostic log\n'):
raise ApiError(415, "unsupported_media", "Invalid diagnostic log.")
attachments.append(('TPSchedule-diagnostics.txt', 'text/plain', data))
return fields, attachments
except HTTPException as exc:
detail = str(exc.detail).lower()