Skip to content

Commit fd06adc

Browse files
authored
fix(glossary): Exportation failed when glossary name has non-ASCII characters (#237)
1 parent 52a73a2 commit fd06adc

1 file changed

Lines changed: 18 additions & 4 deletions

File tree

src/api/blueprints/glossary_routes.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import zipfile
1616
from pathlib import Path
1717
from typing import List, Optional, Tuple
18+
from urllib.parse import quote
1819

1920
from flask import Blueprint, Response, jsonify, request
2021
from lxml import etree
@@ -286,10 +287,24 @@ def _glossary_summary(glossary: Glossary) -> dict:
286287

287288
def _safe_filename(name: str) -> str:
288289
"""Turn a glossary name into a download-safe file stem."""
289-
cleaned = ''.join(c if c.isalnum() or c in ('-', '_', '.') else '_' for c in (name or 'glossary'))
290+
cleaned = re.sub(r'[^A-Za-z0-9._-]+', '_', name or 'glossary')
290291
cleaned = cleaned.strip('._') or 'glossary'
291292
return cleaned
292293

294+
def _attachment_disposition(filename: str) -> str:
295+
"""Build a WSGI-safe attachment header with UTF-8 filename support."""
296+
normalized = (filename or 'glossary').replace('\r', ' ').replace('\n', ' ').strip() or 'glossary'
297+
if '.' in normalized:
298+
stem, suffix = normalized.rsplit('.', 1)
299+
safe_suffix = re.sub(r'[^A-Za-z0-9]+', '', suffix)
300+
ascii_fallback = _safe_filename(stem)
301+
if safe_suffix:
302+
ascii_fallback = f'{ascii_fallback}.{safe_suffix}'
303+
else:
304+
ascii_fallback = _safe_filename(normalized)
305+
encoded = quote(normalized, safe='')
306+
return f'attachment; filename="{ascii_fallback}"; filename*=UTF-8\'\'{encoded}'
307+
293308
def _terms_from_payload(payload) -> List[GlossaryTerm]:
294309
"""Build a list of GlossaryTerm from a JSON payload (list or dict)."""
295310
if isinstance(payload, dict):
@@ -645,7 +660,6 @@ def export_glossary(gid: int):
645660
return jsonify({"error": f"Glossary {gid} not found"}), 404
646661

647662
fmt = (request.args.get('format') or 'json').lower()
648-
stem = _safe_filename(glossary.name)
649663

650664
if fmt == 'csv':
651665
buffer = io.StringIO()
@@ -661,7 +675,7 @@ def export_glossary(gid: int):
661675
buffer.getvalue(),
662676
mimetype='text/csv; charset=utf-8',
663677
headers={
664-
'Content-Disposition': f'attachment; filename={stem}.csv'
678+
'Content-Disposition': _attachment_disposition(f'{glossary.name or "glossary"}.csv')
665679
},
666680
)
667681

@@ -677,7 +691,7 @@ def export_glossary(gid: int):
677691
_json.dumps(payload, ensure_ascii=False, indent=2),
678692
mimetype='application/json; charset=utf-8',
679693
headers={
680-
'Content-Disposition': f'attachment; filename={stem}.json'
694+
'Content-Disposition': _attachment_disposition(f'{glossary.name or "glossary"}.json')
681695
},
682696
)
683697

0 commit comments

Comments
 (0)