The route checks were by name, so a link or a Windows spelling could reach a trashed file. _media_access_error now resolves the path and answers not found, and the autotag target, rescope, annotations, collections, the exact-path listing and preview making refuse it too. Delete and Return do not use the gate.
61 lines
2.5 KiB
Python
61 lines
2.5 KiB
Python
"""The listing identity of one media file, as a request names it.
|
|
|
|
Shared by the image-annotation routes and the collection routes (#331): a
|
|
target is ``{root_index, root_scope, path}``, checked here without touching
|
|
the filesystem or the database. The scope is compared with the current roots
|
|
by the caller.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
|
|
from .images import _file_not_found, _forbidden, _has_traversal
|
|
from .prompts import bad_request
|
|
|
|
try:
|
|
from ...utils.media_capture import CAPTURE_EXTENSIONS
|
|
from ...utils.media_folders import in_trash_folder
|
|
except ImportError:
|
|
from utils.media_capture import CAPTURE_EXTENSIONS
|
|
from utils.media_folders import in_trash_folder
|
|
|
|
MAX_TARGET_PATH = 1024
|
|
TARGET_FIELDS = frozenset({"root_index", "root_scope", "path"})
|
|
_HEX64 = re.compile(r"[0-9a-f]{64}")
|
|
TAGGING_MEDIA_MESSAGE = "Only media files can be tagged"
|
|
|
|
|
|
def media_target(data: object, *, media_message: str = TAGGING_MEDIA_MESSAGE):
|
|
"""``((index, scope, relative), None)`` for a target, else ``(None, error)``.
|
|
|
|
Only the listing identity is accepted: an int root index, the root's
|
|
64-hex scope and a bounded root-relative POSIX media path, never an
|
|
absolute, drive-qualified, backslash or ``..`` path (a 400 for a malformed
|
|
shape, a 403 for an unsafe or non-media path, carrying *media_message*, a
|
|
404 for a path in a trash folder, #543). A link or a short-name spelling
|
|
is refused later by the capture store's alias check.
|
|
"""
|
|
if not isinstance(data, dict) or set(data) != TARGET_FIELDS:
|
|
return None, bad_request("target must name root_index, root_scope and path")
|
|
index, scope, relative = data["root_index"], data["root_scope"], data["path"]
|
|
if type(index) is not int or index < 0:
|
|
return None, bad_request("Invalid gallery root")
|
|
if not isinstance(scope, str) or _HEX64.fullmatch(scope) is None:
|
|
return None, bad_request("Invalid gallery root scope")
|
|
if not isinstance(relative, str) or not relative or len(relative) > MAX_TARGET_PATH:
|
|
return None, bad_request("path must be a relative media path")
|
|
parts = relative.split("/")
|
|
if (
|
|
"\0" in relative
|
|
or "\\" in relative
|
|
or relative.startswith("/")
|
|
or _has_traversal(relative)
|
|
or any(part in ("", ".", "..") or ":" in part for part in parts)
|
|
):
|
|
return None, _forbidden()
|
|
if os.path.splitext(parts[-1])[1].lower() not in CAPTURE_EXTENSIONS:
|
|
return None, _forbidden(media_message)
|
|
if in_trash_folder(relative):
|
|
return None, _file_not_found()
|
|
return (index, scope, relative), None
|