mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-12 19:50:57 +05:00
Sanitize tag export filenames via django.utils.text.slugify
Some checks failed
Build Docker image / buildx (push) Has been cancelled
Run linters / lint (push) Has been cancelled
Build Pip package / build (push) Has been cancelled
Release State / release-state (push) Has been cancelled
Run tests / python_tests (ubuntu-22.04, 3.13) (push) Has been cancelled
Run tests / docker_tests (push) Has been cancelled
Some checks failed
Build Docker image / buildx (push) Has been cancelled
Run linters / lint (push) Has been cancelled
Build Pip package / build (push) Has been cancelled
Release State / release-state (push) Has been cancelled
Run tests / python_tests (ubuntu-22.04, 3.13) (push) Has been cancelled
Run tests / docker_tests (push) Has been cancelled
Addresses review feedback from cubic and devin: quote()'s percent- encoding isn't decoded by browsers in Content-Disposition's filename parameter (Safari saves literal %20). Switch to Django's slugify() which does NFKD normalization, ASCII transliteration, and replaces punctuation with hyphens — producing clean names like "tag-alpha-research-urls.txt". - Add tag_filename_safe(name) helper wrapping slugify - Use it in both tag export endpoints - Drop the now-unneeded JS fallback name (server always sets Content-Disposition)
This commit is contained in:
parent
b83e2de73a
commit
0041a2d407
@ -2,7 +2,6 @@ __package__ = "archivebox.api"
|
||||
|
||||
import math
|
||||
from collections import defaultdict
|
||||
from urllib.parse import quote
|
||||
from uuid import UUID
|
||||
from typing import Union, Any, Annotated
|
||||
from datetime import datetime
|
||||
@ -37,6 +36,7 @@ from archivebox.core.tag_utils import (
|
||||
normalize_has_snapshots_filter,
|
||||
normalize_tag_sort,
|
||||
rename_tag as rename_tag_record,
|
||||
tag_filename_safe,
|
||||
)
|
||||
from archivebox.crawls.models import Crawl
|
||||
from archivebox.api.v1_crawls import CrawlSchema
|
||||
@ -725,8 +725,7 @@ def tag_urls_export(request: HttpRequest, tag_id: int):
|
||||
raise HttpError(404, "Tag not found") from err
|
||||
|
||||
response = HttpResponse(export_tag_urls(tag), content_type="text/plain; charset=utf-8")
|
||||
# TODO: potentially harden this more, e.g. replace all special characters with ANSII equivalents / strip punctuation / etc.
|
||||
response["Content-Disposition"] = f'attachment; filename="tag-{quote(tag.name, safe="")}-urls.txt"'
|
||||
response["Content-Disposition"] = f'attachment; filename="tag-{tag_filename_safe(tag.name)}-urls.txt"'
|
||||
return response
|
||||
|
||||
|
||||
@ -738,8 +737,7 @@ def tag_snapshots_export(request: HttpRequest, tag_id: int):
|
||||
raise HttpError(404, "Tag not found") from err
|
||||
|
||||
response = HttpResponse(export_tag_snapshots_jsonl(tag), content_type="application/x-ndjson; charset=utf-8")
|
||||
# TODO: potentially harden this more, e.g. replace all special characters with ANSII equivalents / strip punctuation / etc.
|
||||
response["Content-Disposition"] = f'attachment; filename="tag-{quote(tag.name, safe="")}-snapshots.jsonl"'
|
||||
response["Content-Disposition"] = f'attachment; filename="tag-{tag_filename_safe(tag.name)}-snapshots.jsonl"'
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ from django.db.models import Count, F, QuerySet
|
||||
from django.db.models.functions import Lower
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.utils.text import slugify
|
||||
|
||||
from archivebox.core.host_utils import build_snapshot_url, build_web_url
|
||||
from archivebox.core.models import Snapshot, SnapshotTag, Tag
|
||||
@ -35,6 +36,11 @@ def normalize_tag_name(name: str) -> str:
|
||||
return (name or "").strip()
|
||||
|
||||
|
||||
def tag_filename_safe(name: str) -> str:
|
||||
"""ASCII-safe filename fragment for a tag name (via django.utils.text.slugify)."""
|
||||
return slugify(name or "") or "tag"
|
||||
|
||||
|
||||
def normalize_tag_sort(sort: str = "created_desc") -> str:
|
||||
valid_sorts = {key for key, _label in TAG_SORT_CHOICES}
|
||||
return sort if sort in valid_sorts else "created_desc"
|
||||
|
||||
@ -974,8 +974,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
if (action === 'download-jsonl') {
|
||||
actionButton.disabled = true;
|
||||
try {
|
||||
const tagName = cardEl.querySelector('.tag-card__display strong')?.textContent || 'tag';
|
||||
await downloadFileFromUrl(cardEl.dataset.exportJsonlUrl, 'tag-' + encodeURIComponent(tagName) + '-snapshots.jsonl');
|
||||
await downloadFileFromUrl(cardEl.dataset.exportJsonlUrl, 'tag-snapshots.jsonl');
|
||||
} catch (error) {
|
||||
setToast(error.message || 'Failed to download JSONL.', 'error');
|
||||
} finally {
|
||||
|
||||
@ -179,11 +179,11 @@ def test_tag_snapshots_export_returns_jsonl(client, api_token, tagged_data):
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
|
||||
from urllib.parse import quote
|
||||
from archivebox.core.tag_utils import tag_filename_safe
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"].startswith("application/x-ndjson")
|
||||
assert f"tag-{quote(tag.name, safe='')}-snapshots.jsonl" in response["Content-Disposition"]
|
||||
assert f"tag-{tag_filename_safe(tag.name)}-snapshots.jsonl" in response["Content-Disposition"]
|
||||
body = response.content.decode()
|
||||
assert '"type": "Snapshot"' in body
|
||||
assert '"tags": "Alpha Research"' in body
|
||||
@ -198,10 +198,10 @@ def test_tag_urls_export_returns_plain_text_urls(client, api_token, tagged_data)
|
||||
HTTP_HOST=ADMIN_HOST,
|
||||
)
|
||||
|
||||
from urllib.parse import quote
|
||||
from archivebox.core.tag_utils import tag_filename_safe
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response["Content-Type"].startswith("text/plain")
|
||||
assert f"tag-{quote(tag.name, safe='')}-urls.txt" in response["Content-Disposition"]
|
||||
assert f"tag-{tag_filename_safe(tag.name)}-urls.txt" in response["Content-Disposition"]
|
||||
exported_urls = set(filter(None, response.content.decode().splitlines()))
|
||||
assert exported_urls == {snapshot.url for snapshot in snapshots}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user