diff --git a/packages/server/src/services/executions/index.ts b/packages/server/src/services/executions/index.ts index 6acae3494..f54ee3220 100644 --- a/packages/server/src/services/executions/index.ts +++ b/packages/server/src/services/executions/index.ts @@ -72,7 +72,7 @@ const getAllExecutions = async (filters: ExecutionFilters = {}): Promise<{ data: const queryBuilder = appServer.AppDataSource.getRepository(Execution) .createQueryBuilder('execution') .leftJoinAndSelect('execution.agentflow', 'agentflow') - .orderBy('execution.createdDate', 'DESC') + .orderBy('execution.updatedDate', 'DESC') .skip((page - 1) * limit) .take(limit) diff --git a/packages/ui/package.json b/packages/ui/package.json index 133c1ea43..73ff9c2f0 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -36,6 +36,7 @@ "@uiw/react-codemirror": "^4.21.21", "axios": "1.7.9", "clsx": "^1.1.1", + "dompurify": "^3.2.6", "dotenv": "^16.0.0", "flowise-embed": "latest", "flowise-embed-react": "latest", diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index 77e398b21..aba719726 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -44,6 +44,7 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' // Project import import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' +import { SafeHTML } from '@/ui-component/safe/SafeHTML' import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog' import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown' import { StyledButton } from '@/ui-component/button/StyledButton' @@ -860,7 +861,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { } else if (item.type === 'html') { return (
-
+
) } else { diff --git a/packages/ui/src/ui-component/json/JsonViewer.jsx b/packages/ui/src/ui-component/json/JsonViewer.jsx index 823478e61..6d6b72bc7 100644 --- a/packages/ui/src/ui-component/json/JsonViewer.jsx +++ b/packages/ui/src/ui-component/json/JsonViewer.jsx @@ -3,31 +3,85 @@ import { Box } from '@mui/material' import { useTheme } from '@mui/material/styles' import PropTypes from 'prop-types' -// Syntax highlighting function for JSON -function syntaxHighlight(json) { - if (!json) return '' // No JSON from response - - json = json.replace(/&/g, '&').replace(//g, '>') - - return json.replace( - // eslint-disable-next-line - /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, - function (match) { - let cls = 'number' - if (/^"/.test(match)) { - if (/:$/.test(match)) { - cls = 'key' - } else { - cls = 'string' - } - } else if (/true|false/.test(match)) { - cls = 'boolean' - } else if (/null/.test(match)) { - cls = 'null' - } - return '' + match + '' +const JsonToken = ({ type, children, isDarkMode }) => { + const getTokenStyle = (tokenType) => { + switch (tokenType) { + case 'string': + return { color: isDarkMode ? '#9cdcfe' : 'green' } + case 'number': + return { color: isDarkMode ? '#b5cea8' : 'darkorange' } + case 'boolean': + return { color: isDarkMode ? '#569cd6' : 'blue' } + case 'null': + return { color: isDarkMode ? '#d4d4d4' : 'magenta' } + case 'key': + return { color: isDarkMode ? '#ff5733' : '#ff5733' } + default: + return {} } - ) + } + + return {children} +} + +function parseJsonToElements(json, isDarkMode) { + if (!json) return [] + + const tokens = [] + let index = 0 + + // Escape HTML characters for safety + const escapedJson = json.replace(/&/g, '&').replace(//g, '>') + + // eslint-disable-next-line + const tokenRegex = /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g + + let match + let lastIndex = 0 + + while ((match = tokenRegex.exec(escapedJson)) !== null) { + // Add any text before the match as plain text + if (match.index > lastIndex) { + const plainText = escapedJson.substring(lastIndex, match.index) + if (plainText) { + tokens.push({plainText}) + } + } + + // Determine token type + let tokenType = 'number' + const matchText = match[0] + + if (/^"/.test(matchText)) { + if (/:$/.test(matchText)) { + tokenType = 'key' + } else { + tokenType = 'string' + } + } else if (/true|false/.test(matchText)) { + tokenType = 'boolean' + } else if (/null/.test(matchText)) { + tokenType = 'null' + } + + tokens.push( + + {matchText} + + ) + + lastIndex = match.index + match[0].length + } + + // Add any remaining text + if (lastIndex < escapedJson.length) { + const remainingText = escapedJson.substring(lastIndex) + if (remainingText) { + tokens.push({remainingText}) + } + } + + return tokens } export const JSONViewer = ({ data, maxHeight = '400px' }) => { @@ -35,6 +89,9 @@ export const JSONViewer = ({ data, maxHeight = '400px' }) => { const customization = useSelector((state) => state.customization) const isDarkMode = customization.isDarkMode + const jsonString = JSON.stringify(data, null, 2) + const jsonElements = parseJsonToElements(jsonString, isDarkMode) + return ( { maxHeight: maxHeight }} > -
 {
                     whiteSpace: 'pre-wrap',
                     wordBreak: 'break-word'
                 }}
-                dangerouslySetInnerHTML={{
-                    __html: syntaxHighlight(JSON.stringify(data, null, 2), isDarkMode)
-                }}
-            />
+            >
+                {jsonElements}
+            
) } @@ -85,3 +124,9 @@ JSONViewer.propTypes = { data: PropTypes.object, maxHeight: PropTypes.string } + +JsonToken.propTypes = { + type: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + isDarkMode: PropTypes.bool.isRequired +} diff --git a/packages/ui/src/ui-component/safe/SafeHTML.jsx b/packages/ui/src/ui-component/safe/SafeHTML.jsx new file mode 100644 index 000000000..3ef11b3fe --- /dev/null +++ b/packages/ui/src/ui-component/safe/SafeHTML.jsx @@ -0,0 +1,58 @@ +import PropTypes from 'prop-types' +import DOMPurify from 'dompurify' + +/** + * SafeHTML component that sanitizes HTML content before rendering + */ +export const SafeHTML = ({ html, allowedTags, allowedAttributes, ...props }) => { + // Configure DOMPurify options + const config = { + ALLOWED_TAGS: allowedTags || [ + 'p', + 'br', + 'strong', + 'em', + 'u', + 'i', + 'b', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'ul', + 'ol', + 'li', + 'blockquote', + 'pre', + 'code', + 'a', + 'img', + 'table', + 'thead', + 'tbody', + 'tr', + 'th', + 'td', + 'div', + 'span' + ], + ALLOWED_ATTR: allowedAttributes || ['href', 'title', 'alt', 'src', 'class', 'id', 'style'], + ALLOW_DATA_ATTR: false, + FORBID_SCRIPT: true, + FORBID_TAGS: ['script', 'object', 'embed', 'form', 'input'], + FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'] + } + + // Sanitize the HTML content + const sanitizedHTML = DOMPurify.sanitize(html || '', config) + + return
+} + +SafeHTML.propTypes = { + html: PropTypes.string.isRequired, + allowedTags: PropTypes.arrayOf(PropTypes.string), + allowedAttributes: PropTypes.arrayOf(PropTypes.string) +} diff --git a/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx b/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx index 5a5296912..5be0f4c93 100644 --- a/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx +++ b/packages/ui/src/views/agentexecutions/NodeExecutionDetails.jsx @@ -29,6 +29,7 @@ import toolSVG from '@/assets/images/tool.svg' // Project imports import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' +import { SafeHTML } from '@/ui-component/safe/SafeHTML' import { AGENTFLOW_ICONS, baseURL } from '@/store/constant' import { JSONViewer } from '@/ui-component/json/JsonViewer' import ReactJson from 'flowise-react-json-view' @@ -708,7 +709,7 @@ export const NodeExecutionDetails = ({ data, label, status, metadata, isPublic, backgroundColor: theme.palette.background.paper }} > -
+ ) } else { diff --git a/packages/ui/src/views/chatmessage/ChatMessage.jsx b/packages/ui/src/views/chatmessage/ChatMessage.jsx index 34a5e402b..b3eb5ed27 100644 --- a/packages/ui/src/views/chatmessage/ChatMessage.jsx +++ b/packages/ui/src/views/chatmessage/ChatMessage.jsx @@ -49,6 +49,7 @@ import audioUploadSVG from '@/assets/images/wave-sound.jpg' // project import import NodeInputHandler from '@/views/canvas/NodeInputHandler' import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' +import { SafeHTML } from '@/ui-component/safe/SafeHTML' import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog' import ChatFeedbackContentDialog from '@/ui-component/dialog/ChatFeedbackContentDialog' import StarterPromptsCard from '@/ui-component/cards/StarterPromptsCard' @@ -1659,7 +1660,7 @@ const ChatMessage = ({ open, chatflowid, isAgentCanvas, isDialog, previews, setP } else if (item.type === 'html') { return (
-
+
) } else { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fafa45f26..865ce3dc1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -984,6 +984,9 @@ importers: clsx: specifier: ^1.1.1 version: 1.2.1 + dompurify: + specifier: ^3.2.6 + version: 3.2.6 dotenv: specifier: ^16.0.0 version: 16.4.5 @@ -9941,6 +9944,9 @@ packages: resolution: { integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== } engines: { node: '>= 4' } + dompurify@3.2.6: + resolution: { integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ== } + domutils@1.7.0: resolution: { integrity: sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== } @@ -30197,6 +30203,10 @@ snapshots: dependencies: domelementtype: 2.3.0 + dompurify@3.2.6: + optionalDependencies: + '@types/trusted-types': 2.0.7 + domutils@1.7.0: dependencies: dom-serializer: 0.2.2