mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-03 19:15:37 +08:00
Compare commits
2 Commits
fix/plugin
...
chore/use-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdfd576ae7 | ||
|
|
d9e9df183f |
@@ -5,6 +5,9 @@
|
|||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||||
<title>CloudCLI UI</title>
|
<title>CloudCLI UI</title>
|
||||||
|
|
||||||
<!-- PWA Manifest -->
|
<!-- PWA Manifest -->
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -39,6 +39,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
|
"dompurify": "^3.4.7",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
@@ -4580,6 +4581,13 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@types/unist": {
|
"node_modules/@types/unist": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
@@ -7485,6 +7493,15 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.4.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz",
|
||||||
|
"integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dot-prop": {
|
"node_modules/dot-prop": {
|
||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz",
|
||||||
|
|||||||
@@ -96,6 +96,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
|
"dompurify": "^3.4.7",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
import { authenticatedFetch } from '../../../utils/api';
|
import { authenticatedFetch } from '../../../utils/api';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -10,6 +12,48 @@ type Props = {
|
|||||||
// Module-level cache so repeated renders don't re-fetch
|
// Module-level cache so repeated renders don't re-fetch
|
||||||
const svgCache = new Map<string, string>();
|
const svgCache = new Map<string, string>();
|
||||||
|
|
||||||
|
const FORBIDDEN_SVG_TAGS = [
|
||||||
|
'script',
|
||||||
|
'foreignObject',
|
||||||
|
'iframe',
|
||||||
|
'object',
|
||||||
|
'embed',
|
||||||
|
'link',
|
||||||
|
'meta',
|
||||||
|
'style',
|
||||||
|
'animate',
|
||||||
|
'set',
|
||||||
|
'animateTransform',
|
||||||
|
'animateMotion',
|
||||||
|
];
|
||||||
|
|
||||||
|
const FORBIDDEN_SVG_ATTRS = [
|
||||||
|
'href',
|
||||||
|
'xlink:href',
|
||||||
|
'src',
|
||||||
|
'style',
|
||||||
|
];
|
||||||
|
|
||||||
|
function sanitizeSvg(svgText: string): string | null {
|
||||||
|
const sanitized = DOMPurify.sanitize(svgText, {
|
||||||
|
USE_PROFILES: { svg: true, svgFilters: true },
|
||||||
|
FORBID_TAGS: FORBIDDEN_SVG_TAGS,
|
||||||
|
FORBID_ATTR: FORBIDDEN_SVG_ATTRS,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!sanitized) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const doc = new DOMParser().parseFromString(sanitized, 'image/svg+xml');
|
||||||
|
const root = doc.documentElement;
|
||||||
|
if (!root || root.nodeName.toLowerCase() !== 'svg') return null;
|
||||||
|
if (doc.querySelector('parsererror')) return null;
|
||||||
|
return sanitized;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
||||||
const url = iconFile
|
const url = iconFile
|
||||||
? `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`
|
? `/api/plugins/${encodeURIComponent(pluginName)}/assets/${encodeURIComponent(iconFile)}`
|
||||||
@@ -24,9 +68,11 @@ export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
|||||||
return r.text();
|
return r.text();
|
||||||
})
|
})
|
||||||
.then((text) => {
|
.then((text) => {
|
||||||
if (text && text.trimStart().startsWith('<svg')) {
|
if (!text) return;
|
||||||
svgCache.set(url, text);
|
const sanitized = sanitizeSvg(text);
|
||||||
setSvg(text);
|
if (sanitized) {
|
||||||
|
svgCache.set(url, sanitized);
|
||||||
|
setSvg(sanitized);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
@@ -35,10 +81,6 @@ export default function PluginIcon({ pluginName, iconFile, className }: Props) {
|
|||||||
if (!svg) return <span className={className} />;
|
if (!svg) return <span className={className} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span className={className} dangerouslySetInnerHTML={{ __html: svg }} />
|
||||||
className={className}
|
|
||||||
// SVG is fetched from the user's own installed plugin — same trust level as the plugin code itself
|
|
||||||
dangerouslySetInnerHTML={{ __html: svg }}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: "Montserrat", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user