Files
homarr/packages/widgets/src/custom-api/jsx-whitelist.ts
T
2026-06-10 18:58:14 +02:00

193 lines
4.2 KiB
TypeScript

import type { ComponentType } from "react";
import { createElement } from "react";
import {
Stack,
Group,
Flex,
Grid,
SimpleGrid,
Center,
Space,
Container,
AspectRatio,
Text,
Title,
Code,
Highlight,
Mark,
Kbd,
Blockquote,
Anchor,
NumberFormatter,
Badge,
Card,
Paper,
Alert,
ThemeIcon,
ColorSwatch,
Table,
List,
Timeline,
Accordion,
Indicator,
Pill,
Spoiler,
Progress,
RingProgress,
Skeleton,
Loader,
Image,
Avatar,
BackgroundImage,
Tooltip,
Divider,
ScrollArea,
} from "@mantine/core";
import {
AreaChart,
BarChart,
LineChart,
DonutChart,
PieChart,
RadarChart,
RadialBarChart,
Sparkline,
} from "@mantine/charts";
import { PaginatedList, TabsContainer, TabPanel, Collapsible, StatBar, TypeBadge } from "./jsx-interactive-components";
const SAFE_URL_PATTERN = /^https?:\/\//i;
function isSafeUrl(url: unknown): boolean {
if (typeof url !== "string") return false;
return SAFE_URL_PATTERN.test(url) || url.startsWith("/") || url.startsWith("#");
}
function SafeAnchor(props: Record<string, unknown>) {
const href = props.href;
const safeHref = href && isSafeUrl(href) ? href : undefined;
return createElement(Anchor as never, { ...props, href: safeHref });
}
export const WHITELISTED_COMPONENTS: Record<string, ComponentType<never>> = {
Stack,
Group,
Flex,
Grid,
"Grid.Col": Grid.Col,
SimpleGrid,
Center,
Space,
Container,
AspectRatio,
Text,
Title,
Code,
Highlight,
Mark,
Kbd,
Blockquote,
Anchor: SafeAnchor,
NumberFormatter,
Badge,
Card,
"Card.Section": Card.Section,
Paper,
Alert,
ThemeIcon,
ColorSwatch,
Table,
"Table.Thead": Table.Thead,
"Table.Tbody": Table.Tbody,
"Table.Tr": Table.Tr,
"Table.Th": Table.Th,
"Table.Td": Table.Td,
List,
"List.Item": List.Item,
Timeline,
"Timeline.Item": Timeline.Item,
Accordion,
"Accordion.Item": Accordion.Item,
"Accordion.Control": Accordion.Control,
"Accordion.Panel": Accordion.Panel,
Indicator,
Pill,
Spoiler,
Progress,
"Progress.Section": Progress.Section,
RingProgress,
Skeleton,
Loader,
Image,
Avatar,
"Avatar.Group": Avatar.Group,
BackgroundImage,
Tooltip,
Divider,
ScrollArea,
AreaChart,
BarChart,
LineChart,
DonutChart,
PieChart,
RadarChart,
RadialBarChart,
Sparkline,
PaginatedList,
TabsContainer,
TabPanel,
Collapsible,
StatBar,
TypeBadge,
};
function sanitizeData(obj: unknown): unknown {
if (obj === null || obj === undefined) return obj;
if (typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(sanitizeData);
const safe: Record<string, unknown> = Object.create(null);
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
if (key === "constructor" || key === "__proto__" || key === "prototype") continue;
safe[key] = sanitizeData(value);
}
return safe;
}
const safeMath: Record<string, unknown> = Object.create(null);
safeMath.round = (v: number) => Math.round(v);
safeMath.floor = (v: number) => Math.floor(v);
safeMath.ceil = (v: number) => Math.ceil(v);
safeMath.abs = (v: number) => Math.abs(v);
safeMath.min = (...args: number[]) => Math.min(...args);
safeMath.max = (...args: number[]) => Math.max(...args);
safeMath.pow = (b: number, e: number) => Math.pow(b, e);
safeMath.sqrt = (v: number) => Math.sqrt(v);
safeMath.PI = Math.PI;
Object.freeze(safeMath);
const safeJSON: Record<string, unknown> = Object.create(null);
safeJSON.stringify = (v: unknown) => JSON.stringify(v);
Object.freeze(safeJSON);
const safeArray: Record<string, unknown> = Object.create(null);
safeArray.isArray = (v: unknown) => Array.isArray(v);
Object.freeze(safeArray);
const safeObject: Record<string, unknown> = Object.create(null);
safeObject.keys = (v: object) => Object.keys(v);
safeObject.values = (v: object) => Object.values(v);
safeObject.entries = (v: object) => Object.entries(v);
Object.freeze(safeObject);
export const SAFE_BINDINGS = (apiData: unknown) => ({
data: sanitizeData(apiData),
String: (v: unknown) => String(v),
Number: (v: unknown) => Number(v),
Boolean: (v: unknown) => Boolean(v),
Math: safeMath,
JSON: safeJSON,
Array: safeArray,
Object: safeObject,
});