From f15d932d548a557d090b8827897d8f1fdfadbb1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Bachmann?= Date: Thu, 15 Jan 2026 12:23:38 +0000 Subject: [PATCH] Tes --- custom_components/bahmcloud_store/views.py | 83 +++++++++++++++++----- 1 file changed, 64 insertions(+), 19 deletions(-) diff --git a/custom_components/bahmcloud_store/views.py b/custom_components/bahmcloud_store/views.py index 979f670..4aca65e 100644 --- a/custom_components/bahmcloud_store/views.py +++ b/custom_components/bahmcloud_store/views.py @@ -1,5 +1,6 @@ from __future__ import annotations +import base64 import logging from dataclasses import asdict from pathlib import Path @@ -17,8 +18,8 @@ _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). + Server-side rendering is required because HA frontend context for custom panels + may not expose marked/DOMPurify/ha-markdown consistently. """ text = (md or "").strip() if not text: @@ -26,7 +27,7 @@ def _render_markdown_server_side(md: str) -> str | None: html: str | None = None - # 1) Try python-markdown (commonly available in HA) + # 1) Try python-markdown try: import markdown as mdlib # type: ignore @@ -47,12 +48,11 @@ def _render_markdown_server_side(md: str) -> str | None: if not html: return None - # 2) Sanitize via bleach if available (preferred) + # 2) Sanitize via bleach if available try: import bleach # type: ignore allowed_tags = [ - # structure "p", "br", "hr", @@ -61,28 +61,23 @@ def _render_markdown_server_side(md: str) -> str | None: "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", @@ -107,25 +102,66 @@ def _render_markdown_server_side(md: str) -> str | None: strip=True, ) - # Make links safe by default (no opener) - sanitized = sanitized.replace(' str | None: + """ + Ensure README is always a plain markdown string. + + Handles cases where upstream functions return: + - str (already fine) + - dict from GitHub/GitLab "contents" endpoints (base64 content) + - dict with a nested 'readme' field + """ + if maybe_md is None: + return None + + if isinstance(maybe_md, str): + return maybe_md + + if isinstance(maybe_md, dict): + # Common pattern: { "content": "", "encoding": "base64" } + content = maybe_md.get("content") + encoding = maybe_md.get("encoding") + + if isinstance(content, str) and isinstance(encoding, str) and encoding.lower() == "base64": + try: + raw = base64.b64decode(content.encode("utf-8"), validate=False) + return raw.decode("utf-8", errors="replace") + except Exception: + pass + + # Another common pattern: { "readme": "..." } + nested = maybe_md.get("readme") + if isinstance(nested, str): + return nested + + # Some APIs use { "content": "plain text" } without encoding + if isinstance(content, str) and not encoding: + return content + + # Anything else (list/int/etc.) is unsupported + return None + + class StaticAssetsView(HomeAssistantView): url = "/api/bahmcloud_store_static/{path:.*}" name = "api:bahmcloud_store_static" - # Keep this as you currently have it working (no auth for static) + # Keep your working behavior (static assets must load without auth for panel modules) requires_auth = False async def get(self, request: web.Request, path: str) -> web.Response: @@ -138,6 +174,7 @@ class StaticAssetsView(HomeAssistantView): target = (base / req_path).resolve() + # Prevent traversal if not str(target).startswith(str(base_resolved)): return web.Response(status=404) @@ -148,6 +185,7 @@ class StaticAssetsView(HomeAssistantView): _LOGGER.error("BCS static asset not found: %s", target) return web.Response(status=404) + # IMPORTANT: content_type must NOT include charset (aiohttp restriction) content_type = "text/plain" charset = None @@ -233,10 +271,17 @@ class BCSReadmeView(HomeAssistantView): if not repo_id: return web.json_response({"ok": False, "message": "Missing repo_id"}, status=400) - md = await self.core.fetch_readme_markdown(repo_id) + maybe_md = await self.core.fetch_readme_markdown(repo_id) + + md = _coerce_readme_to_text(maybe_md) if not md: - return web.json_response({"ok": False, "message": "README not found."}, status=404) + # Provide a useful debug hint without leaking internal objects + t = type(maybe_md).__name__ + return web.json_response( + {"ok": False, "message": f"README not found or invalid format (got {t})."}, + status=404, + ) html = _render_markdown_server_side(md) - return web.json_response({"ok": True, "readme": md, "html": html}) + return web.json_response({"ok": True, "readme": md, "html": html}) \ No newline at end of file