77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
"""پاکسازی ورودیهای غنی در زمان ذخیره.
|
|
|
|
بدنه مقالهها سند TipTap (ProseMirror) است و فرانتاند همان را با
|
|
renderToHTMLString به HTML تبدیل و مستقیم تزریق میکند؛ متن گرهها در آن
|
|
مسیر escape میشود، اما آدرسهای href/src عیناً وارد HTML میشوند — پس
|
|
اسکیمهای خطرناک (javascript: و data: و ...) باید در سمت سرور حذف شوند.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
from urllib.parse import urlparse
|
|
|
|
# اسکیمهای مجاز برای href/src — مسیرهای نسبی و لنگر هم بیاسکیماند و میگذرند
|
|
SAFE_URL_SCHEMES = {"http", "https", "mailto", "tel"}
|
|
URL_ATTRS = ("href", "src")
|
|
|
|
|
|
def _clean_url(value: Any) -> str | None:
|
|
"""آدرس امن را برمیگرداند؛ در غیر این صورت None (مشخصه حذف میشود)."""
|
|
if not isinstance(value, str):
|
|
return value if value is not None else None
|
|
stripped = value.strip().replace("\n", "").replace("\r", "").replace("\t", "")
|
|
if not stripped:
|
|
return None
|
|
parsed = urlparse(stripped)
|
|
if parsed.scheme and parsed.scheme.lower() not in SAFE_URL_SCHEMES:
|
|
return None
|
|
return stripped
|
|
|
|
|
|
def _is_junk_paragraph(node: Any) -> bool:
|
|
"""پاراگراف خالی یا فقط نویسهٔ خنثی (nbsp/فاصله) — داخل نقلقول حذف میشود."""
|
|
if not isinstance(node, dict) or node.get("type") != "paragraph":
|
|
return False
|
|
content = node.get("content") or []
|
|
if not content:
|
|
return True
|
|
return all(
|
|
isinstance(item, dict)
|
|
and item.get("type") == "text"
|
|
and not str(item.get("text", "")).replace("\xa0", " ").strip()
|
|
for item in content
|
|
)
|
|
|
|
|
|
def _walk(node: Any) -> Any:
|
|
if isinstance(node, list):
|
|
return [_walk(item) for item in node]
|
|
if not isinstance(node, dict):
|
|
return node
|
|
cleaned: dict[str, Any] = {}
|
|
for key, value in node.items():
|
|
if key == "attrs" and isinstance(value, dict):
|
|
attrs = dict(value)
|
|
for attr in URL_ATTRS:
|
|
if attr in attrs:
|
|
cleaned_url = _clean_url(attrs[attr])
|
|
if cleaned_url is None:
|
|
attrs.pop(attr, None)
|
|
else:
|
|
attrs[attr] = cleaned_url
|
|
cleaned[key] = attrs
|
|
else:
|
|
cleaned[key] = _walk(value)
|
|
# داخل نقلقول: پاراگرافهای خالی/نویسهٔ خنثی حذف — وگرنه خط خالی اضافه رندر میشود
|
|
if cleaned.get("type") == "blockquote" and isinstance(cleaned.get("content"), list):
|
|
cleaned["content"] = [c for c in cleaned["content"] if not _is_junk_paragraph(c)]
|
|
return cleaned
|
|
|
|
|
|
def sanitize_doc(doc: Any) -> Any:
|
|
"""سند TipTap را درجا پاک میکند (سایر ساختارها دستنخورده برمیگردند)."""
|
|
if not isinstance(doc, dict):
|
|
return doc
|
|
return _walk(doc)
|