custom_components/bahmcloud_store/views.py aktualisiert
This commit is contained in:
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user