custom_components/bahmcloud_store/views.py aktualisiert

This commit is contained in:
2026-01-15 11:46:56 +00:00
parent b5e98898e0
commit f60b3a8730

View File

@@ -14,13 +14,118 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
def _render_markdown_server_side(md: str) -> str | None:
"""
Render Markdown -> sanitized HTML.
We do NOT rely on Home Assistant internal markdown utilities (they may change),
and we do NOT rely on frontend JS libs (marked/DOMPurify may be unavailable).
"""
text = (md or "").strip()
if not text:
return None
html: str | None = None
# 1) Try python-markdown (commonly available in HA)
try:
import markdown as mdlib # type: ignore
html = mdlib.markdown(
text,
extensions=[
"fenced_code",
"tables",
"sane_lists",
"toc",
],
output_format="html5",
)
except Exception as e:
_LOGGER.debug("python-markdown render failed: %s", e)
html = None
if not html:
return None
# 2) Sanitize via bleach if available (preferred)
try:
import bleach # type: ignore
allowed_tags = [
# structure
"p",
"br",
"hr",
"div",
"span",
"blockquote",
"pre",
"code",
# headings
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
# lists
"ul",
"ol",
"li",
# emphasis
"strong",
"em",
"b",
"i",
"u",
"s",
# links/images
"a",
"img",
# tables
"table",
"thead",
"tbody",
"tr",
"th",
"td",
]
allowed_attrs = {
"a": ["href", "title", "target", "rel"],
"img": ["src", "alt", "title"],
"th": ["align"],
"td": ["align"],
"*": ["class"],
}
sanitized = bleach.clean(
html,
tags=allowed_tags,
attributes=allowed_attrs,
protocols=["http", "https", "mailto"],
strip=True,
)
# Make links safe by default (no opener)
sanitized = sanitized.replace('<a href="', '<a rel="noreferrer noopener" target="_blank" href="')
return sanitized
except Exception as e:
_LOGGER.debug("bleach sanitize failed/unavailable: %s", e)
# 3) If bleach is not available, return raw rendered HTML as last resort
# (still okay-ish because Markdown renderer does not execute scripts by itself,
# but sanitization is strongly preferred).
return html
class StaticAssetsView(HomeAssistantView):
url = "/api/bahmcloud_store_static/{path:.*}"
name = "api:bahmcloud_store_static"
# CRITICAL:
# Panel static files (JS/CSS/HTML) must be accessible without auth.
# Otherwise module loads can fail behind proxies/clients and trigger HA's ban middleware.
# Keep this as you currently have it working (no auth for static)
requires_auth = False
async def get(self, request: web.Request, path: str) -> web.Response:
@@ -33,7 +138,6 @@ class StaticAssetsView(HomeAssistantView):
target = (base / req_path).resolve()
# Prevent path traversal
if not str(target).startswith(str(base_resolved)):
return web.Response(status=404)
@@ -44,7 +148,6 @@ class StaticAssetsView(HomeAssistantView):
_LOGGER.error("BCS static asset not found: %s", target)
return web.Response(status=404)
# aiohttp: do NOT include charset in content_type string
content_type = "text/plain"
charset = None
@@ -63,8 +166,6 @@ class StaticAssetsView(HomeAssistantView):
content_type = "image/png"
resp = web.Response(body=target.read_bytes(), content_type=content_type, charset=charset)
# Development friendly (prevents stale cached panel.js)
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
resp.headers["Pragma"] = "no-cache"
return resp
@@ -80,7 +181,11 @@ class BCSApiView(HomeAssistantView):
async def get(self, request: web.Request) -> web.Response:
return web.json_response(
{"ok": True, "version": self.core.version, "repos": self.core.list_repos_public()}
{
"ok": True,
"version": self.core.version,
"repos": self.core.list_repos_public(),
}
)
async def post(self, request: web.Request) -> web.Response:
@@ -132,19 +237,6 @@ class BCSReadmeView(HomeAssistantView):
if not md:
return web.json_response({"ok": False, "message": "README not found."}, status=404)
# For now keep html best-effort (we will perfect it after the store is stable again)
html = None
try:
from homeassistant.util.markdown import async_render_markdown # type: ignore
html = await async_render_markdown(self.core.hass, md)
except Exception as e:
_LOGGER.debug("Markdown render failed: %s", e)
if html:
try:
from homeassistant.util.sanitize_html import async_sanitize_html # type: ignore
html = await async_sanitize_html(self.core.hass, html)
except Exception as e:
_LOGGER.debug("HTML sanitize not available/failed: %s", e)
html = _render_markdown_server_side(md)
return web.json_response({"ok": True, "readme": md, "html": html})