98 lines
2.3 KiB
Python
98 lines
2.3 KiB
Python
import html
|
|
from html.parser import HTMLParser
|
|
|
|
|
|
_ALLOWED_TAGS = {
|
|
"b",
|
|
"strong",
|
|
"i",
|
|
"em",
|
|
"u",
|
|
"p",
|
|
"br",
|
|
"ul",
|
|
"ol",
|
|
"li",
|
|
"blockquote",
|
|
"code",
|
|
"pre",
|
|
"a",
|
|
"h1",
|
|
"h2",
|
|
"h3",
|
|
"h4",
|
|
"h5",
|
|
"h6",
|
|
}
|
|
|
|
_ALLOWED_ATTRS = {
|
|
"a": {"href", "title", "target", "rel"},
|
|
}
|
|
|
|
_ALLOWED_SCHEMES = ("http://", "https://", "mailto:")
|
|
|
|
|
|
def _escape_attr(value: str) -> str:
|
|
return html.escape(value, quote=True)
|
|
|
|
|
|
def sanitize_html_fragment(raw: str) -> str:
|
|
class SafeHtmlParser(HTMLParser):
|
|
def __init__(self):
|
|
super().__init__(convert_charrefs=True)
|
|
self.parts = []
|
|
|
|
def handle_starttag(self, tag, attrs):
|
|
tag = tag.lower()
|
|
if tag not in _ALLOWED_TAGS:
|
|
return
|
|
|
|
allowed = _ALLOWED_ATTRS.get(tag, set())
|
|
clean_attrs = []
|
|
|
|
for key, value in attrs:
|
|
key = (key or "").lower()
|
|
value = value or ""
|
|
|
|
if key not in allowed:
|
|
continue
|
|
|
|
if tag == "a" and key == "href":
|
|
lowered = value.strip().lower()
|
|
if not lowered.startswith(_ALLOWED_SCHEMES):
|
|
continue
|
|
|
|
clean_attrs.append((key, value))
|
|
|
|
if tag == "a":
|
|
attr_keys = {key for key, _ in clean_attrs}
|
|
if "target" not in attr_keys:
|
|
clean_attrs.append(("target", "_blank"))
|
|
if "rel" not in attr_keys:
|
|
clean_attrs.append(("rel", "noopener noreferrer"))
|
|
|
|
attr_text = "".join(
|
|
f' {key}="{_escape_attr(value)}"'
|
|
for key, value in clean_attrs
|
|
)
|
|
self.parts.append(f"<{tag}{attr_text}>")
|
|
|
|
def handle_endtag(self, tag):
|
|
tag = tag.lower()
|
|
if tag in _ALLOWED_TAGS and tag not in {"br"}:
|
|
self.parts.append(f"</{tag}>")
|
|
|
|
def handle_data(self, data):
|
|
self.parts.append(html.escape(data))
|
|
|
|
def handle_entityref(self, name):
|
|
self.parts.append(f"&{name};")
|
|
|
|
def handle_charref(self, name):
|
|
self.parts.append(f"&#{name};")
|
|
|
|
parser = SafeHtmlParser()
|
|
parser.feed(raw or "")
|
|
parser.close()
|
|
return "".join(parser.parts)
|