diff --git a/apps/frontend/src/renderer/components/AppUpdateNotification.tsx b/apps/frontend/src/renderer/components/AppUpdateNotification.tsx index 7bd00f30..535f919e 100644 --- a/apps/frontend/src/renderer/components/AppUpdateNotification.tsx +++ b/apps/frontend/src/renderer/components/AppUpdateNotification.tsx @@ -1,6 +1,8 @@ import { useState, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Download, RefreshCw, CheckCircle2, AlertCircle, ExternalLink } from "lucide-react"; +import ReactMarkdown, { type Components } from "react-markdown"; +import remarkGfm from "remark-gfm"; import { Button } from "./ui/button"; import { Progress } from "./ui/progress"; import { @@ -16,58 +18,52 @@ import type { AppUpdateAvailableEvent, AppUpdateProgress } from "../../shared/ty const CLAUDE_CODE_CHANGELOG_URL = "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md"; -/** - * Simple markdown renderer for release notes - * Handles: headers, bold, lists, line breaks - */ -function ReleaseNotesRenderer({ markdown }: { markdown: string }) { - const html = useMemo(() => { - const result = markdown - // Escape HTML - .replace(/&/g, "&") - .replace(//g, ">") - // Headers (### Header ->

) - .replace( - /^### (.+)$/gm, - '

$1

' - ) - .replace( - /^## (.+)$/gm, - '

$1

' - ) - // Bold (**text** -> ) - .replace(/\*\*([^*]+)\*\*/g, '$1') - // Inline code (`code` -> ) - .replace(/`([^`]+)`/g, '$1') - // List items (- item ->
  • ) - .replace( - /^- (.+)$/gm, - "
  • $1
  • " - ) - // Wrap consecutive list items - .replace(/(]*>.*?<\/li>\n?)+/g, '
      $&
    ') - // Line breaks for remaining lines - .replace(/\n\n/g, '
    ') - .replace(/\n/g, "
    "); +// createSafeLink - factory function that creates a SafeLink component with i18n support +const createSafeLink = (opensInNewWindowText: string) => { + return function SafeLink({ + href, + children, + ...props + }: React.AnchorHTMLAttributes) { + // Validate URL - only allow http, https, and relative links + const isValidUrl = + href && + (href.startsWith("http://") || + href.startsWith("https://") || + href.startsWith("/") || + href.startsWith("#")); - return result; - }, [markdown]); + if (!isValidUrl) { + // For invalid or potentially malicious URLs, render as plain text + return {children}; + } - return ( -
    - ); -} + // External links get security attributes and accessibility indicator + const isExternal = href?.startsWith("http://") || href?.startsWith("https://"); + + return ( + + {children} + {isExternal && {opensInNewWindowText}} + + ); + }; +}; /** * App Update Notification Dialog * Shows when a new app version is available and handles download/install workflow */ export function AppUpdateNotification() { - const { t } = useTranslation(["dialogs"]); + const { t } = useTranslation(["dialogs", "common"]); const [isOpen, setIsOpen] = useState(false); const [updateInfo, setUpdateInfo] = useState(null); const [downloadProgress, setDownloadProgress] = useState(null); @@ -75,6 +71,14 @@ export function AppUpdateNotification() { const [isDownloaded, setIsDownloaded] = useState(false); const [downloadError, setDownloadError] = useState(null); + // Create markdown components with translated accessibility text + const markdownComponents: Components = useMemo( + () => ({ + a: createSafeLink(t("common:accessibility.opensInNewWindow")), + }), + [t] + ); + // Listen for update available event useEffect(() => { const cleanup = window.electronAPI.onAppUpdateAvailable((info) => { @@ -184,7 +188,11 @@ export function AppUpdateNotification() { {/* Release Notes */} {updateInfo.releaseNotes && (
    - +
    + + {updateInfo.releaseNotes} + +
    )}