diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index 736890dc72f..137695f4f88 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -23165,8 +23165,7 @@
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz",
"integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==",
- "optional": true,
- "peer": true,
+ "devOptional": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -23182,8 +23181,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "optional": true,
- "peer": true
+ "devOptional": true
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
@@ -23751,7 +23749,6 @@
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/aphrodite/-/aphrodite-1.2.5.tgz",
"integrity": "sha1-g1jDbIC7A67puXFlqqcBhiJbSYM=",
- "peer": true,
"dependencies": {
"asap": "^2.0.3",
"inline-style-prefixer": "^3.0.1",
@@ -25351,8 +25348,7 @@
"node_modules/bowser": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz",
- "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==",
- "peer": true
+ "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ=="
},
"node_modules/boxen": {
"version": "5.1.2",
@@ -28248,7 +28244,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
- "peer": true,
"dependencies": {
"hyphenate-style-name": "^1.0.2",
"isobject": "^3.0.1"
@@ -29915,23 +29910,6 @@
"topojson": "^1.6.19"
}
},
- "node_modules/datatables.net": {
- "version": "1.11.3",
- "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.3.tgz",
- "integrity": "sha512-VMj5qEaTebpNurySkM6jy6sGpl+s6onPK8xJhYr296R/vUBnz1+id16NVqNf9z5aR076OGcpGHCuiTuy4E05oQ==",
- "dependencies": {
- "jquery": ">=1.7"
- }
- },
- "node_modules/datatables.net-bs": {
- "version": "1.11.3",
- "resolved": "https://registry.npmjs.org/datatables.net-bs/-/datatables.net-bs-1.11.3.tgz",
- "integrity": "sha512-Db1YwAhO0QAWQbZTsKriUrOInT66+xaA+fV616KTKpQt5Zt+p6OsEKK+xv8LxLgG8qu5dPwMBlkhqSiS/hV2sg==",
- "dependencies": {
- "datatables.net": ">=1.10.25",
- "jquery": ">=1.7"
- }
- },
"node_modules/date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
@@ -36621,8 +36599,7 @@
"node_modules/hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
- "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==",
- "peer": true
+ "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"node_modules/iconv-lite": {
"version": "0.4.24",
@@ -37056,7 +37033,6 @@
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz",
"integrity": "sha1-hVG45bTVcyROZqNLBPfTIHaitTQ=",
- "peer": true,
"dependencies": {
"bowser": "^1.7.3",
"css-in-js-utils": "^2.0.0"
@@ -54244,8 +54220,7 @@
"node_modules/string-hash": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
- "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=",
- "peer": true
+ "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs="
},
"node_modules/string-length": {
"version": "4.0.1",
@@ -58989,9 +58964,9 @@
}
},
"node_modules/xss": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz",
- "integrity": "sha512-qmoqrRksmzqSKvgqzN0055UFWY7OKx1/9JWeRswwEVX9fCG5jcYRxa/A2DHcmZX6VJvjzHRQ2STeeVcQkrmLSw==",
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz",
+ "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
"dependencies": {
"commander": "^2.20.3",
"cssfilter": "0.0.10"
@@ -60278,7 +60253,8 @@
"reselect": "^4.0.0",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
- "whatwg-fetch": "^3.0.0"
+ "whatwg-fetch": "^3.0.0",
+ "xss": "^1.0.14"
},
"devDependencies": {
"@emotion/styled": "^11.3.0",
@@ -64620,6 +64596,7 @@
"@vx/scale": "0.0.140",
"@vx/shape": "0.0.140",
"@vx/tooltip": "0.0.140",
+ "aphrodite": "^1.2.0",
"d3-array": "^1.2.0",
"d3-format": "^1.2.0",
"d3-selection": "^1.1.0",
@@ -76494,7 +76471,8 @@
"resize-observer-polyfill": "1.5.1",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
- "whatwg-fetch": "^3.0.0"
+ "whatwg-fetch": "^3.0.0",
+ "xss": "^1.0.14"
},
"dependencies": {
"@testing-library/react-hooks": {
@@ -80287,13 +80265,15 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"devOptional": true,
- "requires": {},
+ "requires": {
+ "ajv": "^8.0.0"
+ },
"dependencies": {
"ajv": {
- "version": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz",
+ "version": "8.8.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.8.2.tgz",
"integrity": "sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==",
- "optional": true,
- "peer": true,
+ "devOptional": true,
"requires": {
"fast-deep-equal": "^3.1.1",
"json-schema-traverse": "^1.0.0",
@@ -80305,8 +80285,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "optional": true,
- "peer": true
+ "devOptional": true
}
}
},
@@ -80737,7 +80716,6 @@
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/aphrodite/-/aphrodite-1.2.5.tgz",
"integrity": "sha1-g1jDbIC7A67puXFlqqcBhiJbSYM=",
- "peer": true,
"requires": {
"asap": "^2.0.3",
"inline-style-prefixer": "^3.0.1",
@@ -81973,8 +81951,7 @@
"bowser": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/bowser/-/bowser-1.9.4.tgz",
- "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ==",
- "peer": true
+ "integrity": "sha512-9IdMmj2KjigRq6oWhmwv1W36pDuA4STQZ8q6YO9um+x07xgYNCD3Oou+WP/3L1HNz7iqythGet3/p4wvc8AAwQ=="
},
"boxen": {
"version": "5.1.2",
@@ -84274,7 +84251,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-2.0.1.tgz",
"integrity": "sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==",
- "peer": true,
"requires": {
"hyphenate-style-name": "^1.0.2",
"isobject": "^3.0.1"
@@ -85501,23 +85477,6 @@
"topojson": "^1.6.19"
}
},
- "datatables.net": {
- "version": "1.11.3",
- "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-1.11.3.tgz",
- "integrity": "sha512-VMj5qEaTebpNurySkM6jy6sGpl+s6onPK8xJhYr296R/vUBnz1+id16NVqNf9z5aR076OGcpGHCuiTuy4E05oQ==",
- "requires": {
- "jquery": ">=1.7"
- }
- },
- "datatables.net-bs": {
- "version": "1.11.3",
- "resolved": "https://registry.npmjs.org/datatables.net-bs/-/datatables.net-bs-1.11.3.tgz",
- "integrity": "sha512-Db1YwAhO0QAWQbZTsKriUrOInT66+xaA+fV616KTKpQt5Zt+p6OsEKK+xv8LxLgG8qu5dPwMBlkhqSiS/hV2sg==",
- "requires": {
- "datatables.net": ">=1.10.25",
- "jquery": ">=1.7"
- }
- },
"date-fns": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
@@ -90666,8 +90625,7 @@
"hyphenate-style-name": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz",
- "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==",
- "peer": true
+ "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ=="
},
"iconv-lite": {
"version": "0.4.24",
@@ -90985,7 +90943,6 @@
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-3.0.8.tgz",
"integrity": "sha1-hVG45bTVcyROZqNLBPfTIHaitTQ=",
- "peer": true,
"requires": {
"bowser": "^1.7.3",
"css-in-js-utils": "^2.0.0"
@@ -101381,7 +101338,8 @@
"integrity": "sha512-JZUw7hBsAHXK7PTyErJyI7SopSBFRcFHDjWW5SWjcugY0i6iH7f+eJkY8cJmGMlZ1C9xz1J3Vjz0plFpavVeRg==",
"requires": {
"@babel/runtime": "^7.2.0",
- "invariant": "^2.2.4"
+ "invariant": "^2.2.4",
+ "prop-types": "^15.5.7"
}
},
"react-split": {
@@ -104185,8 +104143,7 @@
"string-hash": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz",
- "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=",
- "peer": true
+ "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs="
},
"string-length": {
"version": "4.0.1",
@@ -107735,9 +107692,9 @@
"dev": true
},
"xss": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.10.tgz",
- "integrity": "sha512-qmoqrRksmzqSKvgqzN0055UFWY7OKx1/9JWeRswwEVX9fCG5jcYRxa/A2DHcmZX6VJvjzHRQ2STeeVcQkrmLSw==",
+ "version": "1.0.14",
+ "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.14.tgz",
+ "integrity": "sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw==",
"requires": {
"commander": "^2.20.3",
"cssfilter": "0.0.10"
@@ -107976,6 +107933,8 @@
"is-scoped": "^2.1.0",
"lodash": "^4.17.10",
"log-symbols": "^4.0.0",
+ "mem-fs": "^1.2.0 || ^2.0.0",
+ "mem-fs-editor": "^8.1.2 || ^9.0.0",
"minimatch": "^3.0.4",
"npmlog": "^5.0.1",
"p-queue": "^6.6.2",
diff --git a/superset-frontend/packages/superset-ui-core/package.json b/superset-frontend/packages/superset-ui-core/package.json
index 59894f71694..62deb7709b6 100644
--- a/superset-frontend/packages/superset-ui-core/package.json
+++ b/superset-frontend/packages/superset-ui-core/package.json
@@ -60,7 +60,8 @@
"reselect": "^4.0.0",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",
- "whatwg-fetch": "^3.0.0"
+ "whatwg-fetch": "^3.0.0",
+ "xss": "^1.0.14"
},
"devDependencies": {
"@emotion/styled": "^11.3.0",
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx
new file mode 100644
index 00000000000..8fd06cb6f8e
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/utils/html.test.tsx
@@ -0,0 +1,113 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import React from 'react';
+import {
+ sanitizeHtml,
+ isProbablyHTML,
+ sanitizeHtmlIfNeeded,
+ safeHtmlSpan,
+ removeHTMLTags,
+} from './html';
+
+describe('sanitizeHtml', () => {
+ test('should sanitize the HTML string', () => {
+ const htmlString = '';
+ const sanitizedString = sanitizeHtml(htmlString);
+ expect(sanitizedString).not.toContain('script');
+ });
+});
+
+describe('isProbablyHTML', () => {
+ test('should return true if the text contains HTML tags', () => {
+ const htmlText = '
Some HTML content
';
+ const isHTML = isProbablyHTML(htmlText);
+ expect(isHTML).toBe(true);
+ });
+
+ test('should return false if the text does not contain HTML tags', () => {
+ const plainText = 'Just a plain text';
+ const isHTML = isProbablyHTML(plainText);
+ expect(isHTML).toBe(false);
+ });
+});
+
+describe('sanitizeHtmlIfNeeded', () => {
+ test('should sanitize the HTML string if it contains HTML tags', () => {
+ const htmlString = 'Some HTML content
';
+ const sanitizedString = sanitizeHtmlIfNeeded(htmlString);
+ expect(sanitizedString).toEqual(htmlString);
+ });
+
+ test('should return the string as is if it does not contain HTML tags', () => {
+ const plainText = 'Just a plain text';
+ const sanitizedString = sanitizeHtmlIfNeeded(plainText);
+ expect(sanitizedString).toEqual(plainText);
+ });
+});
+
+describe('safeHtmlSpan', () => {
+ test('should return a safe HTML span when the input is HTML', () => {
+ const htmlString = 'Some HTML content
';
+ const safeSpan = safeHtmlSpan(htmlString);
+ expect(safeSpan).toEqual(
+ ,
+ );
+ });
+
+ test('should return the input string as is when it is not HTML', () => {
+ const plainText = 'Just a plain text';
+ const result = safeHtmlSpan(plainText);
+ expect(result).toEqual(plainText);
+ });
+});
+
+describe('removeHTMLTags', () => {
+ test('should remove HTML tags from the string', () => {
+ const input = 'Hello, World!
';
+ const output = removeHTMLTags(input);
+ expect(output).toBe('Hello, World!');
+ });
+
+ test('should return the same string when no HTML tags are present', () => {
+ const input = 'This is a plain text.';
+ const output = removeHTMLTags(input);
+ expect(output).toBe('This is a plain text.');
+ });
+
+ test('should remove nested HTML tags and return combined text content', () => {
+ const input = '';
+ const output = removeHTMLTags(input);
+ expect(output).toBe('TitleContent');
+ });
+
+ test('should handle self-closing tags and return an empty string', () => {
+ const input = '
';
+ const output = removeHTMLTags(input);
+ expect(output).toBe('');
+ });
+
+ test('should handle malformed HTML tags and remove only well-formed tags', () => {
+ const input = 'Unclosed tag';
+ const output = removeHTMLTags(input);
+ expect(output).toBe('Unclosed tag');
+ });
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/html.tsx b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx
new file mode 100644
index 00000000000..3215eb9b9de
--- /dev/null
+++ b/superset-frontend/packages/superset-ui-core/src/utils/html.tsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { FilterXSS, getDefaultWhiteList } from 'xss';
+
+const xssFilter = new FilterXSS({
+ whiteList: {
+ ...getDefaultWhiteList(),
+ span: ['style', 'class', 'title'],
+ div: ['style', 'class'],
+ a: ['style', 'class', 'href', 'title', 'target'],
+ img: ['style', 'class', 'src', 'alt', 'title', 'width', 'height'],
+ video: [
+ 'autoplay',
+ 'controls',
+ 'loop',
+ 'preload',
+ 'src',
+ 'height',
+ 'width',
+ 'muted',
+ ],
+ },
+ stripIgnoreTag: true,
+ css: false,
+});
+
+export function sanitizeHtml(htmlString: string) {
+ return xssFilter.process(htmlString);
+}
+
+export function isProbablyHTML(text: string) {
+ return /<[^>]+>/.test(text);
+}
+
+export function sanitizeHtmlIfNeeded(htmlString: string) {
+ return isProbablyHTML(htmlString) ? sanitizeHtml(htmlString) : htmlString;
+}
+
+export function safeHtmlSpan(possiblyHtmlString: string) {
+ const isHtml = isProbablyHTML(possiblyHtmlString);
+ if (isHtml) {
+ return (
+
+ );
+ }
+ return possiblyHtmlString;
+}
+
+export function removeHTMLTags(str: string): string {
+ return str.replace(/<[^>]*>/g, '');
+}
diff --git a/superset-frontend/packages/superset-ui-core/src/utils/index.ts b/superset-frontend/packages/superset-ui-core/src/utils/index.ts
index 4efc3dedb65..32fa88251ee 100644
--- a/superset-frontend/packages/superset-ui-core/src/utils/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/utils/index.ts
@@ -31,3 +31,4 @@ export { getSelectedText } from './getSelectedText';
export * from './featureFlags';
export * from './random';
export * from './typedMemo';
+export * from './html';
diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/components/Tooltip.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/components/Tooltip.tsx
index 9b20113448b..d61c4844acf 100644
--- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/components/Tooltip.tsx
+++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/components/Tooltip.tsx
@@ -17,9 +17,8 @@
* under the License.
*/
-import { styled } from '@superset-ui/core';
-import React, { useMemo } from 'react';
-import { filterXSS } from 'xss';
+import { styled, safeHtmlSpan } from '@superset-ui/core';
+import React from 'react';
export type TooltipProps = {
tooltip:
@@ -55,28 +54,12 @@ export default function Tooltip(props: TooltipProps) {
}
const { x, y, content } = tooltip;
-
- if (typeof content === 'string') {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const contentHtml = useMemo(
- () => ({
- __html: filterXSS(content, { stripIgnoreTag: true }),
- }),
- [content],
- );
- return (
-
-
-
- );
- }
+ const safeContent =
+ typeof content === 'string' ? safeHtmlSpan(content) : content;
return (
- {content}
+ {safeContent}
);
}
diff --git a/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts b/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
index 327e48ab3d8..607afa8ac39 100644
--- a/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
+++ b/superset-frontend/plugins/plugin-chart-table/src/utils/formatValue.ts
@@ -16,41 +16,16 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { FilterXSS, getDefaultWhiteList } from 'xss';
import {
DataRecordValue,
GenericDataType,
getNumberFormatter,
+ isProbablyHTML,
+ sanitizeHtml,
} from '@superset-ui/core';
import { DataColumnMeta } from '../types';
import DateWithFormatter from './DateWithFormatter';
-const xss = new FilterXSS({
- whiteList: {
- ...getDefaultWhiteList(),
- span: ['style', 'class', 'title'],
- div: ['style', 'class'],
- a: ['style', 'class', 'href', 'title', 'target'],
- img: ['style', 'class', 'src', 'alt', 'title', 'width', 'height'],
- video: [
- 'autoplay',
- 'controls',
- 'loop',
- 'preload',
- 'src',
- 'height',
- 'width',
- 'muted',
- ],
- },
- stripIgnoreTag: true,
- css: false,
-});
-
-function isProbablyHTML(text: string) {
- return /<[^>]+>/.test(text);
-}
-
/**
* Format text for cell value.
*/
@@ -76,7 +51,7 @@ function formatValue(
return [false, formatter(value as number)];
}
if (typeof value === 'string') {
- return isProbablyHTML(value) ? [true, xss.process(value)] : [false, value];
+ return isProbablyHTML(value) ? [true, sanitizeHtml(value)] : [false, value];
}
return [false, value.toString()];
}
diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
index 98fe90eafae..73f3a028e93 100644
--- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailMenuItems.tsx
@@ -26,6 +26,7 @@ import {
extractQueryFields,
getChartMetadataRegistry,
QueryFormData,
+ removeHTMLTags,
styled,
t,
} from '@superset-ui/core';
@@ -50,7 +51,21 @@ const DisabledMenuItem = ({ children, ...props }: { children: ReactNode }) => (
);
-const Filter = styled.span`
+const Filter = ({
+ children,
+ stripHTML = false,
+}: {
+ children: ReactNode;
+ stripHTML: boolean;
+}) => {
+ const content =
+ stripHTML && typeof children === 'string'
+ ? removeHTMLTags(children)
+ : children;
+ return {content};
+};
+
+const StyledFilter = styled(Filter)`
${({ theme }) => `
font-weight: ${theme.typography.weights.bold};
color: ${theme.colors.primary.base};
@@ -191,7 +206,7 @@ const DrillDetailMenuItems = ({
onClick={openModal.bind(null, [filter])}
>
{`${DRILL_TO_DETAIL_TEXT} `}
- {filter.formattedVal}
+ {filter.formattedVal}
))}
{filters.length > 1 && (
@@ -202,7 +217,7 @@ const DrillDetailMenuItems = ({
>
{`${DRILL_TO_DETAIL_TEXT} `}
- {t('all')}
+ {t('all')}
)}
diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx
index daf9f3f1cf4..d337e9b013a 100644
--- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx
+++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx
@@ -300,6 +300,7 @@ export default function DrillDetailPane({
}
resizable
virtualize
+ allowHTML
/>
);
diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx
index 1cf880419db..2ca38617fcc 100644
--- a/superset-frontend/src/components/FilterableTable/index.tsx
+++ b/superset-frontend/src/components/FilterableTable/index.tsx
@@ -22,6 +22,7 @@ import { JSONTree } from 'react-json-tree';
import {
getMultipleTextDimensions,
t,
+ safeHtmlSpan,
styled,
useTheme,
} from '@superset-ui/core';
@@ -120,6 +121,7 @@ export interface FilterableTableProps {
// need antd 5.0 to support striped color pattern
striped?: boolean;
expandedColumns?: string[];
+ allowHTML?: boolean;
}
const FilterableTable = ({
@@ -128,6 +130,7 @@ const FilterableTable = ({
height,
filterText = '',
expandedColumns = [],
+ allowHTML = true,
}: FilterableTableProps) => {
const formatTableData = (data: Record[]): Datum[] =>
data.map(row => {
@@ -346,13 +349,17 @@ const FilterableTable = ({
const renderTableCell = (cellData: CellDataType, columnKey: string) => {
const cellNode = getCellContent({ cellData, columnKey });
- const content =
- cellData === null ? {cellNode} : cellNode;
+ if (cellData === null) {
+ return {cellNode};
+ }
const jsonObject = safeJsonObjectParse(cellData);
if (jsonObject) {
return renderJsonModal(cellNode, jsonObject, cellData);
}
- return content;
+ if (allowHTML && typeof cellData === 'string') {
+ return safeHtmlSpan(cellNode);
+ }
+ return cellNode;
};
// exclude the height of the horizontal scroll bar from the height of the table
diff --git a/superset-frontend/src/components/Table/VirtualTable.tsx b/superset-frontend/src/components/Table/VirtualTable.tsx
index 721fd906b46..d8658dde609 100644
--- a/superset-frontend/src/components/Table/VirtualTable.tsx
+++ b/superset-frontend/src/components/Table/VirtualTable.tsx
@@ -25,12 +25,13 @@ import classNames from 'classnames';
import { useResizeDetector } from 'react-resize-detector';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { VariableSizeGrid as Grid } from 'react-window';
-import { useTheme, styled } from '@superset-ui/core';
+import { useTheme, styled, safeHtmlSpan } from '@superset-ui/core';
import { TableSize, ETableAction } from './index';
interface VirtualTableProps extends AntTableProps {
height?: number;
+ allowHTML?: boolean;
}
const StyledCell = styled('div')<{ height?: number }>(
@@ -71,7 +72,15 @@ const MIDDLE = 47;
const VirtualTable = (
props: VirtualTableProps,
) => {
- const { columns, pagination, onChange, height, scroll, size } = props;
+ const {
+ columns,
+ pagination,
+ onChange,
+ height,
+ scroll,
+ size,
+ allowHTML = false,
+ } = props;
const [tableWidth, setTableWidth] = useState(0);
const onResize = useCallback((width: number) => {
setTableWidth(width);
@@ -213,6 +222,10 @@ const VirtualTable = (
content = render(content, data, rowIndex);
}
+ if (allowHTML && typeof content === 'string') {
+ content = safeHtmlSpan(content);
+ }
+
return (
{
* Returns props that should be applied to each row component.
*/
onRow?: AntTableProps['onRow'];
+ /**
+ * Will render html safely if set to true, anchor tags and such. Currently
+ * only supported for virtualize == true
+ */
+ allowHTML?: boolean;
}
const defaultRowSelection: React.Key[] = [];
@@ -249,6 +254,7 @@ export function Table(
onChange = noop,
recordCount,
onRow,
+ allowHTML = false,
} = props;
const wrapperRef = useRef(null);
@@ -405,6 +411,7 @@ export function Table(
scrollToFirstRowOnChange: false,
}),
}}
+ allowHTML={allowHTML}
/>
)}
diff --git a/superset-frontend/src/components/TableCollection/index.tsx b/superset-frontend/src/components/TableCollection/index.tsx
index 88296edf638..bcda5139eb1 100644
--- a/superset-frontend/src/components/TableCollection/index.tsx
+++ b/superset-frontend/src/components/TableCollection/index.tsx
@@ -295,7 +295,6 @@ export default React.memo(
const isWrapText = columnsForWrapText?.includes(
cell.column.Header as string,
);
-
return (
},
+ allowHTML?: boolean,
) => {
const [originalFormattedTimeColumns, setOriginalFormattedTimeColumns] =
useState(getTimeColumns(datasourceId));
@@ -346,6 +348,9 @@ export const useTableColumns = (
) {
return timeFormatter(value);
}
+ if (typeof value === 'string' && allowHTML) {
+ return safeHtmlSpan(value);
+ }
return String(value);
},
...moreConfigs?.[key],
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
index 5c66075750d..b542aad9964 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
@@ -92,6 +92,8 @@ export const SamplesPane = ({
data,
datasourceId,
isVisible,
+ {}, // moreConfig
+ true, // allowHTML
);
const filteredData = useFilteredTableData(filterText, data);
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
index 27d312cc3cc..c2614dfda6c 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
@@ -44,6 +44,8 @@ export const SingleQueryResultPane = ({
data,
datasourceId,
isVisible,
+ {}, // moreConfig
+ true, // allowHTML
);
const filteredData = useFilteredTableData(filterText, data);
|