refactor
This commit is contained in:
parent
2ffc414444
commit
bee7213430
97
profiles.py
97
profiles.py
@ -3,10 +3,8 @@ import json
|
||||
import re
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import markdown
|
||||
|
||||
from sanitizer import sanitize_html_fragment
|
||||
from html.parser import HTMLParser
|
||||
from db import get_db
|
||||
|
||||
DEFAULT_ROLE_NAME_COLORS = {
|
||||
@ -14,6 +12,99 @@ DEFAULT_ROLE_NAME_COLORS = {
|
||||
"moderator": "#8ab4ff",
|
||||
"user": "#f5f5f7",
|
||||
}
|
||||
_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)
|
||||
|
||||
PROFILE_BIO_MODES = {"txt", "md", "html"}
|
||||
THEME_MODES = {"dark", "light"}
|
||||
|
||||
@ -15,6 +15,7 @@ from .common import (
|
||||
now_iso,
|
||||
csrf_valid,
|
||||
rate_limited,
|
||||
current_user,
|
||||
require_user,
|
||||
require_writable_user,
|
||||
normalize_username,
|
||||
@ -30,7 +31,6 @@ from .common import (
|
||||
normalized_hex_color,
|
||||
)
|
||||
|
||||
|
||||
def register_profile_routes(app):
|
||||
@app.route("/api/profile")
|
||||
def api_get_my_profile():
|
||||
|
||||
97
sanitizer.py
97
sanitizer.py
@ -1,97 +0,0 @@
|
||||
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)
|
||||
Loading…
x
Reference in New Issue
Block a user