@@ -7272,10 +7272,141 @@ def handle_get(handler, parsed) -> bool:
72727272 return False
72737273 dashboard_dir = _PLUGIN_STATIC_ROOTS.get(name)
72747274 if dashboard_dir:
7275+ dashboard_path = dashboard_dir.resolve()
7276+
7277+ webui_manifest = manifest.get("webui", {}) if isinstance(manifest.get("webui", {}), dict) else {}
7278+
7279+ def _safe_js_global(value: str) -> str | None:
7280+ """Return a safe dotted JS global name from manifest config."""
7281+ parts = str(value or "").split(".")
7282+ if not parts:
7283+ return None
7284+ if all(re.match(r"^[A-Za-z_$][A-Za-z0-9_$]*$", part or "") for part in parts):
7285+ return ".".join(parts)
7286+ return None
7287+
7288+ def _plugin_inline_payload(
7289+ _webui_manifest=webui_manifest,
7290+ _dashboard_path=dashboard_path,
7291+ ) -> tuple[str, str | None]:
7292+ """Return manifest-declared read-only bootstrap payload script."""
7293+ payload_cfg = _webui_manifest.get("inline_payload", {})
7294+ if not isinstance(payload_cfg, dict):
7295+ return "", None
7296+ global_name = _safe_js_global(payload_cfg.get("global"))
7297+ candidates = payload_cfg.get("candidates", [])
7298+ if not global_name or not isinstance(candidates, list):
7299+ return "", global_name
7300+ for rel in candidates:
7301+ if not isinstance(rel, str):
7302+ continue
7303+ payload_path = (_dashboard_path / rel).resolve()
7304+ try:
7305+ payload_path.relative_to(_dashboard_path)
7306+ except ValueError:
7307+ continue
7308+ if not payload_path.is_file():
7309+ continue
7310+ try:
7311+ payload = json.loads(payload_path.read_text(encoding="utf-8"))
7312+ encoded = json.dumps(payload, ensure_ascii=False).replace("</", "<\\/")
7313+ return f"<script>{global_name}={encoded};</script>\n", global_name
7314+ except Exception:
7315+ logger.debug("Failed to embed dashboard plugin payload from %s", payload_path, exc_info=True)
7316+ return "", global_name
7317+
7318+ def _inline_declared_dist_assets(
7319+ html_text: str,
7320+ _webui_manifest=webui_manifest,
7321+ _manifest=manifest,
7322+ _dashboard_path=dashboard_path,
7323+ _name=name,
7324+ ) -> str:
7325+ """Inline manifest-approved dist CSS/JS for sandboxed WebUI plugin iframes."""
7326+ if _webui_manifest.get("inline_dist_assets") is not True:
7327+ return html_text
7328+ css_rel = _manifest.get("css") or "dist/style.css"
7329+ entry_rel = _manifest.get("entry") or "dist/index.js"
7330+ if isinstance(css_rel, str):
7331+ css_path = (_dashboard_path / css_rel).resolve()
7332+ try:
7333+ css_path.relative_to(_dashboard_path)
7334+ except ValueError:
7335+ css_path = None
7336+ if css_path and css_path.is_file():
7337+ style_text = css_path.read_text(encoding="utf-8").replace("</style", "<\\/style")
7338+ html_text = html_text.replace(
7339+ '<link rel="stylesheet" href="style.css">',
7340+ f"<style>{style_text}</style>",
7341+ 1,
7342+ )
7343+ html_text = html_text.replace(
7344+ f'<link rel="stylesheet" href="/{_name}/style.css">',
7345+ f"<style>{style_text}</style>",
7346+ 1,
7347+ )
7348+ if isinstance(entry_rel, str):
7349+ script_path = (_dashboard_path / entry_rel).resolve()
7350+ try:
7351+ script_path.relative_to(_dashboard_path)
7352+ except ValueError:
7353+ script_path = None
7354+ if script_path and script_path.is_file():
7355+ script_text = script_path.read_text(encoding="utf-8").replace("</script", "<\\/script")
7356+ inline_bundle = f" <script>{script_text}</script>"
7357+ script_marker = '<script src="index.js"></script>'
7358+ if script_marker in html_text:
7359+ return html_text.replace(script_marker, inline_bundle, 1)
7360+ if "</body>" in html_text:
7361+ return html_text.replace("</body>", inline_bundle + "</body>", 1)
7362+ return html_text + inline_bundle
7363+ return html_text
7364+
7365+ def _inject_plugin_payload(
7366+ html_text: str,
7367+ _webui_manifest=webui_manifest,
7368+ _name=name,
7369+ ) -> str:
7370+ payload_script, payload_global = _plugin_inline_payload()
7371+ html_text = _inline_declared_dist_assets(html_text)
7372+ # Generic plugin fallback: a plugin's dist/index.html is served
7373+ # at tab.path (for example /demo), so relative dist assets like
7374+ # "index.js" would resolve to /index.js. Rebase known bundle
7375+ # assets to the authenticated dashboard-plugin route unless
7376+ # the manifest requested inline self-containment above.
7377+ name_escaped_for_path = _name.replace('"', '')
7378+ if _webui_manifest.get("inline_dist_assets") is not True:
7379+ html_text = html_text.replace(
7380+ 'href="style.css"',
7381+ f'href="/dashboard-plugins/{name_escaped_for_path}/dist/style.css"',
7382+ 1,
7383+ )
7384+ script_marker = '<script src="index.js"></script>'
7385+ rebased_script = f'<script src="/dashboard-plugins/{name_escaped_for_path}/dist/index.js"></script>'
7386+ if script_marker in html_text:
7387+ replacement = rebased_script
7388+ html_text = html_text.replace(script_marker, replacement, 1)
7389+ payload_assignment = f"{payload_global}=" if payload_global else ""
7390+ payload_already_injected = False
7391+ if payload_assignment and payload_global:
7392+ payload_re = re.compile(
7393+ rf"<script\b[^>]*>[^<]*{re.escape(payload_global)}\s*=",
7394+ re.I | re.S,
7395+ )
7396+ payload_already_injected = bool(payload_re.search(html_text))
7397+ if payload_script and not payload_already_injected:
7398+ first_script = html_text.find("<script")
7399+ if first_script >= 0:
7400+ return html_text[:first_script] + payload_script + html_text[first_script:]
7401+ if "</body>" in html_text:
7402+ return html_text.replace("</body>", payload_script + "</body>", 1)
7403+ return html_text + payload_script
7404+ return html_text
7405+
72757406 # 1) dashboard/dist/index.html (full SPA build)
72767407 index_html = dashboard_dir / "dist" / "index.html"
72777408 if index_html.is_file():
7278- data = index_html.read_bytes( )
7409+ data = _inject_plugin_payload( index_html.read_text(encoding="utf-8")).encode("utf-8" )
72797410 handler.send_response(200)
72807411 handler.send_header("Content-Type", "text/html; charset=utf-8")
72817412 handler.send_header("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-popups")
@@ -7287,7 +7418,7 @@ def handle_get(handler, parsed) -> bool:
72877418 plugin_root = dashboard_dir.parent
72887419 static_html = plugin_root / "static" / "index.html"
72897420 if static_html.is_file():
7290- data = static_html.read_bytes( )
7421+ data = _inject_plugin_payload( static_html.read_text(encoding="utf-8")).encode("utf-8" )
72917422 handler.send_response(200)
72927423 handler.send_header("Content-Type", "text/html; charset=utf-8")
72937424 handler.send_header("Content-Security-Policy", "sandbox allow-scripts allow-forms allow-popups")
@@ -7303,6 +7434,7 @@ def handle_get(handler, parsed) -> bool:
73037434 css = html.escape(manifest.get("css", ""))
73047435 name_escaped = html.escape(name)
73057436 css_tag = f'<link rel="stylesheet" href="/dashboard-plugins/{name_escaped}/{css}">' if css else ""
7437+ payload_script, _payload_global = _plugin_inline_payload()
73067438 html_content = (
73077439 f"<!doctype html>\n"
73087440 f"<html lang=\"en\">\n"
@@ -7313,6 +7445,7 @@ def handle_get(handler, parsed) -> bool:
73137445 f"</head>\n"
73147446 f"<body>\n"
73157447 f' <div id="pluginPageContainer"></div>\n'
7448+ f" {payload_script}"
73167449 f' <script src="/dashboard-plugins/{name_escaped}/dist/index.js"></script>\n'
73177450 f"</body>\n"
73187451 f"</html>\n"
0 commit comments