"""پاک‌سازی ورودی‌های غنی در زمان ذخیره. بدنه مقاله‌ها سند 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 _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) return cleaned def sanitize_doc(doc: Any) -> Any: """سند TipTap را درجا پاک می‌کند (سایر ساختارها دست‌نخورده برمی‌گردند).""" if not isinstance(doc, dict): return doc return _walk(doc)