add csp and more function for markdown (#4921)
* support html * html * add csp * remove unuse function --------- Co-authored-by: dreamer6680 <146868355@qq.com>
This commit is contained in:
parent
0f866fc552
commit
d7a722a609
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
@ -46,7 +46,7 @@ importers:
|
||||
version: 10.1.4(socks@2.8.4)
|
||||
next-i18next:
|
||||
specifier: 15.4.2
|
||||
version: 15.4.2(i18next@23.16.8)(next@14.2.28(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 15.4.2(i18next@23.16.8)(next@14.2.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
prettier:
|
||||
specifier: 3.2.4
|
||||
version: 3.2.4
|
||||
@ -343,7 +343,7 @@ importers:
|
||||
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
'@chakra-ui/next-js':
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
|
||||
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
|
||||
'@chakra-ui/react':
|
||||
specifier: 2.10.7
|
||||
version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -406,7 +406,7 @@ importers:
|
||||
version: 4.17.21
|
||||
next-i18next:
|
||||
specifier: 15.4.2
|
||||
version: 15.4.2(i18next@23.16.8)(next@14.2.28(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 15.4.2(i18next@23.16.8)(next@14.2.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
papaparse:
|
||||
specifier: ^5.4.1
|
||||
version: 5.4.1
|
||||
@ -467,7 +467,7 @@ importers:
|
||||
version: 2.1.1(@chakra-ui/system@2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
'@chakra-ui/next-js':
|
||||
specifier: 2.4.2
|
||||
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
|
||||
version: 2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)
|
||||
'@chakra-ui/react':
|
||||
specifier: 2.10.7
|
||||
version: 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@ -569,7 +569,7 @@ importers:
|
||||
version: 14.2.28(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1)
|
||||
next-i18next:
|
||||
specifier: 15.4.2
|
||||
version: 15.4.2(i18next@23.16.8)(next@14.2.28(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
version: 15.4.2(i18next@23.16.8)(next@14.2.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
|
||||
nprogress:
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0
|
||||
@ -612,6 +612,9 @@ importers:
|
||||
rehype-katex:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.1
|
||||
rehype-raw:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
remark-breaks:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
@ -5879,9 +5882,15 @@ packages:
|
||||
hast-util-parse-selector@4.0.0:
|
||||
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||
|
||||
hast-util-raw@9.1.0:
|
||||
resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
resolution: {integrity: sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==}
|
||||
|
||||
hast-util-to-text@4.0.2:
|
||||
resolution: {integrity: sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==}
|
||||
|
||||
@ -5919,6 +5928,9 @@ packages:
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
htmlparser2@8.0.2:
|
||||
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||
|
||||
@ -8026,6 +8038,9 @@ packages:
|
||||
property-information@5.6.0:
|
||||
resolution: {integrity: sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==}
|
||||
|
||||
property-information@6.5.0:
|
||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||
|
||||
property-information@7.0.0:
|
||||
resolution: {integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==}
|
||||
|
||||
@ -8361,6 +8376,9 @@ packages:
|
||||
rehype-katex@7.0.1:
|
||||
resolution: {integrity: sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==}
|
||||
|
||||
rehype-raw@7.0.0:
|
||||
resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==}
|
||||
|
||||
remark-breaks@4.0.0:
|
||||
resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==}
|
||||
|
||||
@ -10969,7 +10987,7 @@ snapshots:
|
||||
'@chakra-ui/system': 2.6.1(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
|
||||
'@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)':
|
||||
'@chakra-ui/next-js@2.4.2(@chakra-ui/react@2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(next@14.2.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@chakra-ui/react': 2.10.7(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@emotion/styled@11.11.0(@emotion/react@11.11.1(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(react@18.3.1))(@types/react@18.3.1)(framer-motion@9.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@emotion/cache': 11.14.0
|
||||
@ -16108,6 +16126,22 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
||||
hast-util-raw@9.1.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
'@ungap/structured-clone': 1.3.0
|
||||
hast-util-from-parse5: 8.0.3
|
||||
hast-util-to-parse5: 8.0.0
|
||||
html-void-elements: 3.0.0
|
||||
mdast-util-to-hast: 13.2.0
|
||||
parse5: 7.2.1
|
||||
unist-util-position: 5.0.0
|
||||
unist-util-visit: 5.0.0
|
||||
vfile: 6.0.3
|
||||
web-namespaces: 2.0.1
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
@ -16128,6 +16162,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
hast-util-to-parse5@8.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
comma-separated-tokens: 2.0.3
|
||||
devlop: 1.1.0
|
||||
property-information: 6.5.0
|
||||
space-separated-tokens: 2.0.2
|
||||
web-namespaces: 2.0.1
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-text@4.0.2:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
@ -16175,6 +16219,8 @@ snapshots:
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
htmlparser2@8.0.2:
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
@ -18238,7 +18284,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
next-i18next@15.4.2(i18next@23.16.8)(next@14.2.28(@babel/core@7.26.10)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||
next-i18next@15.4.2(i18next@23.16.8)(next@14.2.28(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(sass@1.85.1))(react-i18next@14.1.2(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
'@types/hoist-non-react-statics': 3.3.6
|
||||
@ -18873,6 +18919,8 @@ snapshots:
|
||||
dependencies:
|
||||
xtend: 4.0.2
|
||||
|
||||
property-information@6.5.0: {}
|
||||
|
||||
property-information@7.0.0: {}
|
||||
|
||||
proto-list@1.2.4: {}
|
||||
@ -19292,6 +19340,12 @@ snapshots:
|
||||
unist-util-visit-parents: 6.0.1
|
||||
vfile: 6.0.3
|
||||
|
||||
rehype-raw@7.0.0:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
hast-util-raw: 9.1.0
|
||||
vfile: 6.0.3
|
||||
|
||||
remark-breaks@4.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
||||
@ -11,6 +11,53 @@ const nextConfig = {
|
||||
output: 'standalone',
|
||||
reactStrictMode: isDev ? false : true,
|
||||
compress: true,
|
||||
|
||||
headers: async () => {
|
||||
const nonce = Buffer.from(crypto.randomUUID()).toString('base64');
|
||||
const csp = `'nonce-${nonce}'`;
|
||||
const scheme_source = 'data: mediastream: blob: filesystem:';
|
||||
const NECESSARY_DOMAINS = [
|
||||
'*.sentry.io',
|
||||
'http://localhost:*',
|
||||
'http://127.0.0.1:*',
|
||||
'https://analytics.google.com',
|
||||
'googletagmanager.com',
|
||||
'*.googletagmanager.com',
|
||||
'https://www.google-analytics.com',
|
||||
'https://api.github.com'
|
||||
].join(' ');
|
||||
|
||||
return [
|
||||
{
|
||||
source: '/chat/(.*)',
|
||||
headers: [
|
||||
{ key: 'X-Frame-Options', value: 'DENY' },
|
||||
{ key: 'X-Content-Type-Options', value: 'nosniff' },
|
||||
{ key: 'X-XSS-Protection', value: '1; mode=block' },
|
||||
{ key: 'Referrer-Policy', value: 'no-referrer' },
|
||||
{
|
||||
key: 'Content-Security-Policy',
|
||||
value: [
|
||||
`default-src 'self' ${scheme_source} ${NECESSARY_DOMAINS} ${csp}`,
|
||||
`script-src 'self' 'unsafe-inline' 'unsafe-eval' ${csp} ${NECESSARY_DOMAINS}`,
|
||||
`style-src 'self' 'unsafe-inline' ${csp} ${NECESSARY_DOMAINS}`,
|
||||
`media-src 'self' http: ${scheme_source} ${NECESSARY_DOMAINS} ${csp}`,
|
||||
`worker-src 'self' ${csp} ${NECESSARY_DOMAINS} ${scheme_source}`,
|
||||
`img-src * data: blob:`,
|
||||
`font-src 'self'`,
|
||||
`connect-src 'self' wss: https: ${scheme_source} ${NECESSARY_DOMAINS} ${csp}`,
|
||||
"object-src 'none'",
|
||||
"form-action 'self'",
|
||||
"base-uri 'self'",
|
||||
"frame-src 'self' 'allow-scripts'",
|
||||
'sandbox allow-scripts allow-same-origin allow-popups allow-forms',
|
||||
'upgrade-insecure-requests'
|
||||
].join('; ')
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
},
|
||||
webpack(config, { isServer, nextRuntime }) {
|
||||
Object.assign(config.resolve.alias, {
|
||||
'@mongodb-js/zstd': false,
|
||||
@ -85,7 +132,7 @@ const nextConfig = {
|
||||
'pg',
|
||||
'bullmq',
|
||||
'@zilliz/milvus2-sdk-node',
|
||||
"tiktoken",
|
||||
'tiktoken'
|
||||
],
|
||||
outputFileTracingRoot: path.join(__dirname, '../../'),
|
||||
instrumentationHook: true
|
||||
|
||||
@ -60,6 +60,7 @@
|
||||
"recharts": "^2.15.0",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"remark-math": "^6.0.0",
|
||||
|
||||
35
projects/app/src/components/Markdown/errorBoundry.tsx
Normal file
35
projects/app/src/components/Markdown/errorBoundry.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
interface ErrorBoundaryProps {
|
||||
children: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ErrorBoundaryState {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return this.props.fallback || <div>Something went wrong while rendering Markdown.</div>;
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import RemarkMath from 'remark-math'; // Math syntax
|
||||
@ -6,15 +6,15 @@ import RemarkBreaks from 'remark-breaks'; // Line break
|
||||
import RehypeKatex from 'rehype-katex'; // Math render
|
||||
import RemarkGfm from 'remark-gfm'; // Special markdown syntax
|
||||
import RehypeExternalLinks from 'rehype-external-links';
|
||||
|
||||
import RehypeRaw from 'rehype-raw';
|
||||
import styles from './index.module.scss';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { CodeClassNameEnum, mdTextFormat } from './utils';
|
||||
import ErrorBoundary from './errorBoundry';
|
||||
import SVGRenderer from './markdowSVG';
|
||||
import { useCreation } from 'ahooks';
|
||||
import type { AProps } from './A';
|
||||
|
||||
const CodeLight = dynamic(() => import('./codeBlock/CodeLight'), { ssr: false });
|
||||
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
|
||||
const MdImage = dynamic(() => import('./img/Image'), { ssr: false });
|
||||
@ -26,7 +26,40 @@ const AudioBlock = dynamic(() => import('./codeBlock/Audio'), { ssr: false });
|
||||
|
||||
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
|
||||
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
|
||||
const A = dynamic(() => import('./A'), { ssr: false });
|
||||
|
||||
function isSafeHref(href: string): boolean {
|
||||
if (!href) return false;
|
||||
// allow http(s), mailto, tel, relative paths, #, data:image/audio/video
|
||||
return /^(https?:|mailto:|tel:|\/|#|data:(?:image|audio|video))/i.test(href.trim());
|
||||
}
|
||||
|
||||
const SafeA = (props: any) => {
|
||||
const href = props.href || '';
|
||||
const safeHref = isSafeHref(href) ? href : '#';
|
||||
|
||||
const ALLOWED_A_ATTRS = new Set([
|
||||
'href',
|
||||
'target',
|
||||
'rel',
|
||||
'className',
|
||||
'children',
|
||||
'style',
|
||||
'title'
|
||||
]);
|
||||
const safeProps = filterSafeProps(props, ALLOWED_A_ATTRS);
|
||||
|
||||
return (
|
||||
<a
|
||||
{...safeProps}
|
||||
href={safeHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="cursor-pointer underline !decoration-primary-700 decoration-dashed"
|
||||
>
|
||||
{props.children || 'Download'}
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
type Props = {
|
||||
source?: string;
|
||||
@ -36,11 +69,9 @@ type Props = {
|
||||
} & AProps;
|
||||
const Markdown = (props: Props) => {
|
||||
const source = props.source || '';
|
||||
|
||||
if (source.length < 200000) {
|
||||
return <MarkdownRender {...props} />;
|
||||
}
|
||||
|
||||
return <Box whiteSpace={'pre-wrap'}>{source}</Box>;
|
||||
};
|
||||
const MarkdownRender = ({
|
||||
@ -48,7 +79,6 @@ const MarkdownRender = ({
|
||||
showAnimation,
|
||||
isDisabled,
|
||||
forbidZhFormat,
|
||||
|
||||
chatAuthData,
|
||||
onOpenCiteModal
|
||||
}: Props) => {
|
||||
@ -57,54 +87,116 @@ const MarkdownRender = ({
|
||||
img: Image,
|
||||
pre: RewritePre,
|
||||
code: Code,
|
||||
svg: SVGRenderer,
|
||||
script: ScriptBlock,
|
||||
a: (props: any) => (
|
||||
<A
|
||||
<SafeA
|
||||
{...props}
|
||||
showAnimation={showAnimation}
|
||||
chatAuthData={chatAuthData}
|
||||
onOpenCiteModal={onOpenCiteModal}
|
||||
showAnimation={showAnimation}
|
||||
/>
|
||||
)
|
||||
};
|
||||
}, [chatAuthData, onOpenCiteModal, showAnimation]);
|
||||
|
||||
const formatSource = useMemo(() => {
|
||||
if (showAnimation || forbidZhFormat) return source;
|
||||
return mdTextFormat(source);
|
||||
}, [forbidZhFormat, showAnimation, source]);
|
||||
|
||||
const urlTransform = useCallback((val: string) => {
|
||||
return val;
|
||||
}, []);
|
||||
const urlTransform = useCallback((val: string) => val, []);
|
||||
|
||||
return (
|
||||
<Box position={'relative'}>
|
||||
<ErrorBoundary>
|
||||
<ReactMarkdown
|
||||
className={`markdown ${styles.markdown}
|
||||
${showAnimation ? `${formatSource ? styles.waitingAnimation : styles.animation}` : ''}
|
||||
`}
|
||||
className={`markdown ${styles.markdown} ${
|
||||
showAnimation ? `${formatSource ? styles.waitingAnimation : styles.animation}` : ''
|
||||
}`}
|
||||
remarkPlugins={[RemarkMath, [RemarkGfm, { singleTilde: false }], RemarkBreaks]}
|
||||
rehypePlugins={[RehypeKatex, [RehypeExternalLinks, { target: '_blank' }]]}
|
||||
rehypePlugins={[
|
||||
RehypeKatex,
|
||||
[RehypeExternalLinks, { target: '_blank', rel: ['noopener', 'noreferrer'] }],
|
||||
RehypeRaw as any,
|
||||
() => {
|
||||
return (tree) => {
|
||||
const iterate = (node: any) => {
|
||||
if (node.type === 'element') {
|
||||
// delete ref
|
||||
if (node.properties?.ref) delete node.properties.ref;
|
||||
|
||||
// handle invalid tag name
|
||||
if (!/^[a-z][a-z0-9]*$/i.test(node.tagName)) {
|
||||
node.type = 'text';
|
||||
node.value = `<${node.tagName}`;
|
||||
}
|
||||
|
||||
// handle properties, filter events
|
||||
if (node.properties) {
|
||||
Object.keys(node.properties).forEach((key) => {
|
||||
const keyLower = key.toLowerCase();
|
||||
// if event property (on开头)
|
||||
if (keyLower.startsWith('on')) {
|
||||
const value = node.properties[key];
|
||||
// if event value is not a function or contains suspicious content, delete the event
|
||||
if (
|
||||
typeof value === 'string' || // delete event handler in string format
|
||||
value === null ||
|
||||
value === undefined ||
|
||||
(typeof value === 'string' &&
|
||||
(value.includes('javascript:') ||
|
||||
value.includes('alert') ||
|
||||
value.includes('eval') ||
|
||||
value.includes('Function') ||
|
||||
/[\(\)\[\]\{\}]/.test(value))) // flag for executable code containing parentheses, etc.
|
||||
) {
|
||||
delete node.properties[key];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// recursive handle child nodes
|
||||
if (node.children) node.children.forEach(iterate);
|
||||
};
|
||||
tree.children.forEach(iterate);
|
||||
};
|
||||
}
|
||||
]}
|
||||
disallowedElements={[
|
||||
'iframe',
|
||||
'head',
|
||||
'html',
|
||||
'meta',
|
||||
'link',
|
||||
'style',
|
||||
'body',
|
||||
'embed',
|
||||
'object',
|
||||
'param',
|
||||
'applet',
|
||||
'area',
|
||||
'map',
|
||||
'isindex'
|
||||
]}
|
||||
components={components}
|
||||
urlTransform={urlTransform}
|
||||
>
|
||||
{formatSource}
|
||||
</ReactMarkdown>
|
||||
</ErrorBoundary>
|
||||
{isDisabled && <Box position={'absolute'} top={0} right={0} left={0} bottom={0} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Markdown);
|
||||
|
||||
/* Custom dom */
|
||||
function Code(e: any) {
|
||||
const { className, codeBlock, children } = e;
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
const codeType = match?.[1]?.toLowerCase();
|
||||
|
||||
const strChildren = String(children);
|
||||
|
||||
const Component = useMemo(() => {
|
||||
if (codeType === CodeClassNameEnum.mermaid) {
|
||||
return <MermaidCodeBlock code={strChildren} />;
|
||||
@ -134,22 +226,61 @@ function Code(e: any) {
|
||||
if (codeType === CodeClassNameEnum.audio) {
|
||||
return <AudioBlock code={strChildren} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeLight className={className} codeBlock={codeBlock} match={match}>
|
||||
{children}
|
||||
</CodeLight>
|
||||
);
|
||||
}, [codeType, className, codeBlock, match, children, strChildren]);
|
||||
|
||||
return Component;
|
||||
}
|
||||
|
||||
function Image({ src }: { src?: string }) {
|
||||
return <MdImage src={src} />;
|
||||
function sanitizeImageSrc(src?: string): string | undefined {
|
||||
if (!src) return undefined;
|
||||
// remove leading and trailing spaces
|
||||
const trimmed = src.trim();
|
||||
// only allow http/https/data/blob protocols
|
||||
if (/^(https?:|data:|blob:)/i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
// allow relative paths (not starting with javascript: or vbscript:)
|
||||
if (
|
||||
!/^(\w+:)/.test(trimmed) &&
|
||||
!trimmed.startsWith('javascript:') &&
|
||||
!trimmed.startsWith('vbscript:')
|
||||
) {
|
||||
return trimmed;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function RewritePre({ children }: any) {
|
||||
const ALLOWED_IMG_ATTRS = new Set([
|
||||
'alt',
|
||||
'width',
|
||||
'height',
|
||||
'className',
|
||||
'style',
|
||||
'title',
|
||||
'loading',
|
||||
'decoding',
|
||||
'crossOrigin',
|
||||
'referrerPolicy'
|
||||
]);
|
||||
|
||||
function Image({ src, ...rest }: { src?: string; [key: string]: any }) {
|
||||
const safeSrc = sanitizeImageSrc(src);
|
||||
if (!safeSrc) {
|
||||
console.warn(`Blocked unsafe image src: ${src}`);
|
||||
}
|
||||
// only allow whitelist attributes, and remove all on* events
|
||||
const safeProps = filterSafeProps(rest, ALLOWED_IMG_ATTRS);
|
||||
return <MdImage src={safeSrc} {...safeProps} />;
|
||||
}
|
||||
|
||||
function RewritePre({ children, ...rest }: any) {
|
||||
// only allow className, style, etc. safe attributes
|
||||
const ALLOWED_PRE_ATTRS = new Set(['className', 'style']);
|
||||
const safeProps = filterSafeProps(rest, ALLOWED_PRE_ATTRS);
|
||||
const modifiedChildren = React.Children.map(children, (child) => {
|
||||
if (React.isValidElement(child)) {
|
||||
// @ts-ignore
|
||||
@ -157,6 +288,146 @@ function RewritePre({ children }: any) {
|
||||
}
|
||||
return child;
|
||||
});
|
||||
|
||||
return <>{modifiedChildren}</>;
|
||||
return <pre {...safeProps}>{modifiedChildren}</pre>;
|
||||
}
|
||||
|
||||
/**
|
||||
* general safe attribute filter
|
||||
* @param props input props object
|
||||
* @param allowedAttrs allowed attribute name Set
|
||||
* @param eventTypeCheck whether to check event type (e.g. onClick must be a function)
|
||||
*/
|
||||
export function filterSafeProps(
|
||||
props: Record<string, any>,
|
||||
allowedAttrs: Set<string>,
|
||||
eventTypeCheck: boolean = true
|
||||
) {
|
||||
// dangerous protocols
|
||||
const DANGEROUS_PROTOCOLS =
|
||||
/^(?:\s| | )*(?:javascript|vbscript|data(?!:(?:image|audio|video)))/i;
|
||||
|
||||
// dangerous event properties (including various possible ways)
|
||||
const DANGEROUS_EVENTS =
|
||||
/^(?:\s| | )*(?:on|formaction|data-|\[\[|\{\{|xlink:|href|src|action)/i;
|
||||
|
||||
// complete decode function
|
||||
function fullDecode(input: string): string {
|
||||
if (!input) return '';
|
||||
|
||||
let result = input;
|
||||
let lastResult = '';
|
||||
|
||||
// continue decoding until no more decoding can be done
|
||||
while (result !== lastResult) {
|
||||
lastResult = result;
|
||||
try {
|
||||
// HTML entity decode
|
||||
result = result.replace(/&(#?[\w\d]+);/g, (_, entity) => {
|
||||
try {
|
||||
const txt = document.createElement('textarea');
|
||||
txt.innerHTML = `&${entity};`;
|
||||
return txt.value;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
// Unicode decode (\u0061 format)
|
||||
result = result.replace(/(?:\\|%5C|%5c)u([0-9a-f]{4})/gi, (_, hex) =>
|
||||
String.fromCharCode(parseInt(hex, 16))
|
||||
);
|
||||
|
||||
// URL encode decode
|
||||
result = result.replace(/%([0-9a-f]{2})/gi, (_, hex) =>
|
||||
String.fromCharCode(parseInt(hex, 16))
|
||||
);
|
||||
|
||||
// octal decode
|
||||
result = result.replace(/\\([0-7]{3})/gi, (_, oct) =>
|
||||
String.fromCharCode(parseInt(oct, 8))
|
||||
);
|
||||
|
||||
// hexadecimal decode (\x61 format)
|
||||
result = result.replace(/(?:\\|%5C|%5c)x([0-9a-f]{2})/gi, (_, hex) =>
|
||||
String.fromCharCode(parseInt(hex, 16))
|
||||
);
|
||||
|
||||
// handle whitespace and comments
|
||||
result = result.replace(/(?:\s|\/\*.*?\*\/|<!--.*?-->)+/g, '');
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result.toLowerCase();
|
||||
}
|
||||
|
||||
// check if it contains dangerous content
|
||||
function containsDangerousContent(value: string): boolean {
|
||||
const decoded = fullDecode(value);
|
||||
|
||||
return (
|
||||
// check dangerous protocol
|
||||
DANGEROUS_PROTOCOLS.test(decoded) ||
|
||||
// check dangerous event
|
||||
DANGEROUS_EVENTS.test(decoded) ||
|
||||
// check inline event
|
||||
/on\w+\s*=/.test(decoded) ||
|
||||
// check javascript: link
|
||||
/javascript\s*:/.test(decoded) ||
|
||||
// check other possible injections
|
||||
/<\w+/i.test(decoded) ||
|
||||
/\(\s*\)/i.test(decoded) ||
|
||||
/\[\s*\]/i.test(decoded) ||
|
||||
/\{\s*\}/i.test(decoded)
|
||||
);
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(props).filter(([key, value]) => {
|
||||
// 1. decode and check property name
|
||||
const decodedKey = fullDecode(key);
|
||||
|
||||
// 2. intercept all event related properties
|
||||
if (DANGEROUS_EVENTS.test(decodedKey)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 3. all properties not in the whitelist are rejected
|
||||
if (!allowedAttrs.has(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 4. check property value
|
||||
if (typeof value === 'string') {
|
||||
if (containsDangerousContent(value)) {
|
||||
return false;
|
||||
}
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// only allow simple style objects
|
||||
if (key !== 'style') {
|
||||
return false;
|
||||
}
|
||||
// check the value of the style object
|
||||
for (const styleKey in value) {
|
||||
if (containsDangerousContent(String(value[styleKey]))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (typeof value === 'function') {
|
||||
// only allow specific function properties (e.g. onClick)
|
||||
if (!eventTypeCheck || decodedKey !== 'onclick') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const ScriptBlock = memo(({ node }: any) => {
|
||||
const scriptContent = node.children[0]?.value || '';
|
||||
return `<script>${scriptContent}</script>`;
|
||||
});
|
||||
ScriptBlock.displayName = 'ScriptBlock';
|
||||
|
||||
108
projects/app/src/components/Markdown/markdowSVG.tsx
Normal file
108
projects/app/src/components/Markdown/markdowSVG.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import ErrorBoundary from './errorBoundry';
|
||||
import { filterSafeProps } from './index';
|
||||
|
||||
interface SVGProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const SVG_ALLOWED_ATTRS = new Set([
|
||||
'width',
|
||||
'height',
|
||||
'viewBox',
|
||||
'fill',
|
||||
'stroke',
|
||||
'd',
|
||||
'x',
|
||||
'y',
|
||||
'cx',
|
||||
'cy',
|
||||
'r',
|
||||
'className',
|
||||
'style'
|
||||
]);
|
||||
|
||||
const SVGRenderer = ({ children, className, style, ...props }: SVGProps) => {
|
||||
// filter props
|
||||
const svgProps = { ...props, className, style };
|
||||
const sanitizedProps = filterSafeProps(svgProps, SVG_ALLOWED_ATTRS, false);
|
||||
|
||||
const sanitizeSVGContent = (content: string | React.ReactNode): string => {
|
||||
if (typeof content !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
let cleaned = content;
|
||||
|
||||
cleaned = cleaned.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
|
||||
cleaned = cleaned.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '');
|
||||
cleaned = cleaned.replace(
|
||||
/<foreignObject\b[^<]*(?:(?!<\/foreignObject>)<[^<]*)*<\/foreignObject>/gi,
|
||||
''
|
||||
);
|
||||
|
||||
cleaned = cleaned.replace(/\son\w+="[^"]*"/gi, '');
|
||||
cleaned = cleaned.replace(/\son\w+='[^']*'/gi, '');
|
||||
cleaned = cleaned.replace(/url\s*\(\s*['"]?\s*javascript:[^)]+\)/gi, '');
|
||||
cleaned = cleaned.replace(/\bhref="javascript:[^"]*"/gi, '');
|
||||
cleaned = cleaned.replace(/\bhref='javascript:[^']*'/gi, '');
|
||||
cleaned = cleaned.replace(/\bxlink:href="javascript:[^"]*"/gi, '');
|
||||
cleaned = cleaned.replace(/\bxlink:href='javascript:[^']*'/gi, '');
|
||||
cleaned = cleaned.replace(/\bxmlns(:xlink)?=['"]?javascript:[^"']*['"]?/gi, '');
|
||||
cleaned = cleaned.replace(/style\s*=\s*(['"])(?:(?!\1).)*javascript:.*?\1/gi, '');
|
||||
|
||||
cleaned = cleaned.replace(/\bdata:[^,]*?;base64,[^"')]*["')]/gi, (match) => {
|
||||
return match.toLowerCase().includes('javascript') ? '' : match;
|
||||
});
|
||||
|
||||
const ALLOWED_ATTRS = new Set([
|
||||
'width',
|
||||
'height',
|
||||
'viewBox',
|
||||
'fill',
|
||||
'stroke',
|
||||
'd',
|
||||
'x',
|
||||
'y',
|
||||
'cx',
|
||||
'cy',
|
||||
'r',
|
||||
'class',
|
||||
'style'
|
||||
]);
|
||||
cleaned = cleaned.replace(/\s(\w+)=['"][^'"]*['"]/gi, (match, attr) => {
|
||||
return ALLOWED_ATTRS.has(attr.toLowerCase()) ? match : '';
|
||||
});
|
||||
|
||||
cleaned = cleaned.replace(/<!--[\s\S]*?-->/g, '');
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
const sanitizedContent = React.Children.map(children, (child) => {
|
||||
if (typeof child === 'string') {
|
||||
return sanitizeSVGContent(child);
|
||||
}
|
||||
return child;
|
||||
});
|
||||
|
||||
return (
|
||||
<ErrorBoundary fallback={<div>Something went wrong while rendering Markdown.</div>}>
|
||||
<svg
|
||||
{...sanitizedProps}
|
||||
className={className}
|
||||
style={style}
|
||||
dangerouslySetInnerHTML={
|
||||
typeof children === 'string' ? { __html: sanitizeSVGContent(children) } : undefined
|
||||
}
|
||||
>
|
||||
{typeof children !== 'string' && sanitizedContent}
|
||||
</svg>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
export default SVGRenderer;
|
||||
Loading…
x
Reference in New Issue
Block a user