mirror of
https://gitee.com/beecue/fastbee.git
synced 2026-09-20 20:02:06 +08:00
fix(系统): 解决xss前端问题
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; base-uri 'self'; object-src 'none'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://api.map.baidu.com https://*.bdimg.com https://*.baidu.com; style-src 'self' 'unsafe-inline' https://*.bdimg.com https://*.baidu.com; img-src 'self' data: blob: http: https:; font-src 'self' data:; connect-src 'self' http: https: ws: wss:; frame-src 'self' http: https:; worker-src 'self' blob:; media-src 'self' blob: http: https:;">
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||
|
||||
@@ -23,6 +23,7 @@ import "quill/dist/quill.core.css";
|
||||
import "quill/dist/quill.snow.css";
|
||||
import "quill/dist/quill.bubble.css";
|
||||
import { getToken } from "@/utils/auth";
|
||||
import { escapeHtml, sanitizeRichText } from "@/utils/security";
|
||||
|
||||
export default {
|
||||
name: "Editor",
|
||||
@@ -66,6 +67,7 @@ export default {
|
||||
},
|
||||
Quill: null,
|
||||
currentValue: "",
|
||||
sanitizing: false,
|
||||
options: {
|
||||
theme: "snow",
|
||||
bounds: document.body,
|
||||
@@ -82,9 +84,26 @@ export default {
|
||||
[{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色
|
||||
[{ align: [] }], // 对齐方式
|
||||
["clean"], // 清除文本格式
|
||||
["link", "image", "video"] // 链接、图片、视频
|
||||
["link", "image"] // 链接、图片
|
||||
],
|
||||
},
|
||||
formats: [
|
||||
"align",
|
||||
"background",
|
||||
"blockquote",
|
||||
"bold",
|
||||
"code-block",
|
||||
"color",
|
||||
"header",
|
||||
"image",
|
||||
"indent",
|
||||
"italic",
|
||||
"link",
|
||||
"list",
|
||||
"size",
|
||||
"strike",
|
||||
"underline",
|
||||
],
|
||||
placeholder: "请输入内容",
|
||||
readOnly: this.readOnly,
|
||||
},
|
||||
@@ -106,9 +125,9 @@ export default {
|
||||
value: {
|
||||
handler(val) {
|
||||
if (val !== this.currentValue) {
|
||||
this.currentValue = val === null ? "" : val;
|
||||
this.currentValue = sanitizeRichText(val === null ? "" : val);
|
||||
if (this.Quill) {
|
||||
this.Quill.pasteHTML(this.currentValue);
|
||||
this.setEditorHtml(this.currentValue);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -119,6 +138,9 @@ export default {
|
||||
this.init();
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.Quill && this.Quill.root) {
|
||||
this.Quill.root.removeEventListener("paste", this.handlePaste, true);
|
||||
}
|
||||
this.Quill = null;
|
||||
},
|
||||
methods: {
|
||||
@@ -137,11 +159,16 @@ export default {
|
||||
}
|
||||
});
|
||||
}
|
||||
this.Quill.pasteHTML(this.currentValue);
|
||||
this.Quill.root.addEventListener("paste", this.handlePaste, true);
|
||||
this.setEditorHtml(this.currentValue);
|
||||
this.Quill.on("text-change", (delta, oldDelta, source) => {
|
||||
const html = this.$refs.editor.children[0].innerHTML;
|
||||
const rawHtml = this.$refs.editor.children[0].innerHTML;
|
||||
const html = sanitizeRichText(rawHtml);
|
||||
const text = this.Quill.getText();
|
||||
const quill = this.Quill;
|
||||
if (rawHtml !== html && !this.sanitizing) {
|
||||
this.setEditorHtml(html);
|
||||
}
|
||||
this.currentValue = html;
|
||||
this.$emit("input", html);
|
||||
this.$emit("on-change", { html, text, quill });
|
||||
@@ -156,6 +183,38 @@ export default {
|
||||
this.$emit("on-editor-change", eventName, ...args);
|
||||
});
|
||||
},
|
||||
setEditorHtml(value) {
|
||||
if (!this.Quill) {
|
||||
return;
|
||||
}
|
||||
this.sanitizing = true;
|
||||
this.Quill.pasteHTML(sanitizeRichText(value));
|
||||
this.$nextTick(() => {
|
||||
this.sanitizing = false;
|
||||
});
|
||||
},
|
||||
handlePaste(event) {
|
||||
const clipboardData = event.clipboardData || window.clipboardData;
|
||||
if (!clipboardData) {
|
||||
return;
|
||||
}
|
||||
const html = clipboardData.getData("text/html");
|
||||
if (!html) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const text = clipboardData.getData("text/plain");
|
||||
const safeHtml = sanitizeRichText(html) || escapeHtml(text);
|
||||
const range = this.Quill.getSelection(true);
|
||||
const index = range ? range.index : this.Quill.getLength();
|
||||
if (range && range.length) {
|
||||
this.Quill.deleteText(range.index, range.length, "user");
|
||||
}
|
||||
const beforeLength = this.Quill.getLength();
|
||||
this.Quill.clipboard.dangerouslyPasteHTML(index, safeHtml, "user");
|
||||
const pasteLength = Math.max(this.Quill.getLength() - beforeLength, 0);
|
||||
this.Quill.setSelection(index + pasteLength, 0, "user");
|
||||
},
|
||||
// 上传前校检格式和大小
|
||||
handleBeforeUpload(file) {
|
||||
// 校检文件大小
|
||||
@@ -207,10 +266,6 @@ export default {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.ql-snow .ql-tooltip[data-mode="video"]::before {
|
||||
content: "请输入视频地址:";
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
|
||||
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
|
||||
content: "14px";
|
||||
|
||||
30
vue/src/components/SafeHtml/index.vue
Normal file
30
vue/src/components/SafeHtml/index.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<component :is="inline ? 'span' : 'div'" v-html="safeHtml"></component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { sanitizeDisplayHtml, sanitizeRichText } from '@/utils/security';
|
||||
|
||||
export default {
|
||||
name: 'SafeHtml',
|
||||
props: {
|
||||
html: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'display',
|
||||
},
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
safeHtml() {
|
||||
return this.mode === 'richText' ? sanitizeRichText(this.html) : sanitizeDisplayHtml(this.html);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -27,6 +27,8 @@ import Pagination from '@/components/Pagination';
|
||||
import RightToolbar from '@/components/RightToolbar';
|
||||
// 富文本组件
|
||||
import Editor from '@/components/Editor';
|
||||
// 安全HTML渲染组件
|
||||
import SafeHtml from '@/components/SafeHtml';
|
||||
// 文件上传组件
|
||||
import FileUpload from '@/components/FileUpload';
|
||||
// 图片上传组件
|
||||
@@ -88,6 +90,7 @@ Vue.component('DictTag', DictTag);
|
||||
Vue.component('Pagination', Pagination);
|
||||
Vue.component('RightToolbar', RightToolbar);
|
||||
Vue.component('Editor', Editor);
|
||||
Vue.component('SafeHtml', SafeHtml);
|
||||
Vue.component('FileUpload', FileUpload);
|
||||
Vue.component('ImageUpload', ImageUpload);
|
||||
Vue.component('ImagePreview', ImagePreview);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseTime } from './ruoyi'
|
||||
import { sanitizeDisplayHtml } from './security'
|
||||
|
||||
/**
|
||||
* 表格时间格式化
|
||||
@@ -149,7 +150,7 @@ export function param2Obj(url) {
|
||||
*/
|
||||
export function html2Text(val) {
|
||||
const div = document.createElement('div')
|
||||
div.innerHTML = val
|
||||
div.innerHTML = sanitizeDisplayHtml(val)
|
||||
return div.textContent || div.innerText
|
||||
}
|
||||
|
||||
|
||||
280
vue/src/utils/security.js
Normal file
280
vue/src/utils/security.js
Normal file
@@ -0,0 +1,280 @@
|
||||
const DANGEROUS_TAGS = ['script', 'style', 'iframe', 'object', 'embed', 'svg', 'math'];
|
||||
|
||||
const RICH_TEXT_ALLOWED_TAGS = [
|
||||
'a',
|
||||
'b',
|
||||
'blockquote',
|
||||
'br',
|
||||
'code',
|
||||
'div',
|
||||
'em',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'i',
|
||||
'img',
|
||||
'li',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
's',
|
||||
'span',
|
||||
'strong',
|
||||
'u',
|
||||
'ul',
|
||||
];
|
||||
|
||||
const DISPLAY_ALLOWED_TAGS = ['b', 'br', 'code', 'div', 'em', 'i', 'pre', 's', 'span', 'strong', 'u'];
|
||||
|
||||
const GLOBAL_ATTRS = ['class', 'style', 'title'];
|
||||
|
||||
const TAG_ATTRS = {
|
||||
a: ['href', 'target', 'rel'],
|
||||
img: ['src', 'alt', 'width', 'height'],
|
||||
};
|
||||
|
||||
const ALLOWED_STYLE_PROPS = [
|
||||
'background-color',
|
||||
'border',
|
||||
'border-radius',
|
||||
'color',
|
||||
'display',
|
||||
'font-size',
|
||||
'font-style',
|
||||
'font-weight',
|
||||
'height',
|
||||
'line-height',
|
||||
'margin',
|
||||
'margin-bottom',
|
||||
'margin-left',
|
||||
'margin-right',
|
||||
'margin-top',
|
||||
'max-width',
|
||||
'min-width',
|
||||
'overflow',
|
||||
'padding',
|
||||
'text-align',
|
||||
'text-decoration',
|
||||
'white-space',
|
||||
'width',
|
||||
];
|
||||
|
||||
const XSS_RISK_PATTERNS = [
|
||||
/<\s*script\b/i,
|
||||
/<\s*iframe\b/i,
|
||||
/<\s*object\b/i,
|
||||
/<\s*embed\b/i,
|
||||
/<\s*svg\b/i,
|
||||
/<\s*math\b/i,
|
||||
/\bon[a-z]+\s*=/i,
|
||||
/javascript\s*:/i,
|
||||
/vbscript\s*:/i,
|
||||
/data\s*:\s*text\/html/i,
|
||||
/expression\s*\(/i,
|
||||
/url\s*\(\s*['"]?\s*javascript\s*:/i,
|
||||
];
|
||||
|
||||
function includes(list, value) {
|
||||
return list.indexOf(value) !== -1;
|
||||
}
|
||||
|
||||
function decodeHtmlEntities(value) {
|
||||
if (typeof document === 'undefined') {
|
||||
return value;
|
||||
}
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.innerHTML = value;
|
||||
return textarea.value;
|
||||
}
|
||||
|
||||
function normalizeForScan(value) {
|
||||
let text = String(value);
|
||||
try {
|
||||
text = decodeURIComponent(text);
|
||||
} catch (e) {
|
||||
text = String(value);
|
||||
}
|
||||
return decodeHtmlEntities(text)
|
||||
.replace(/[\u0000-\u001F\u007F]/g, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function isSafeUrl(value, allowImageData) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const url = normalizeForScan(value).replace(/[\u0000-\u001F\u007F\s]/g, '');
|
||||
const lowerUrl = url.toLowerCase();
|
||||
|
||||
if (/^(javascript|vbscript):/i.test(lowerUrl)) {
|
||||
return false;
|
||||
}
|
||||
if (/^data:/i.test(lowerUrl)) {
|
||||
return allowImageData && /^data:image\/(png|jpe?g|gif|webp);base64,/i.test(lowerUrl);
|
||||
}
|
||||
if (/^[a-z][a-z0-9+.-]*:/i.test(lowerUrl)) {
|
||||
return /^(https?|mailto|tel):/i.test(lowerUrl);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function sanitizeStyle(styleText) {
|
||||
return String(styleText)
|
||||
.split(';')
|
||||
.map((item) => {
|
||||
const index = item.indexOf(':');
|
||||
if (index === -1) {
|
||||
return '';
|
||||
}
|
||||
const name = item.substring(0, index).trim().toLowerCase();
|
||||
const value = item.substring(index + 1).trim();
|
||||
if (!includes(ALLOWED_STYLE_PROPS, name)) {
|
||||
return '';
|
||||
}
|
||||
if (/expression\s*\(|url\s*\(|javascript\s*:|vbscript\s*:|data\s*:/i.test(value)) {
|
||||
return '';
|
||||
}
|
||||
if (!/^[#(),.%\w\s-]+$/.test(value)) {
|
||||
return '';
|
||||
}
|
||||
return `${name}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
function sanitizeClass(classText) {
|
||||
return String(classText)
|
||||
.split(/\s+/)
|
||||
.filter((className) => /^(ql-|hljs|language-)[\w-]*$/.test(className))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
function isAllowedAttr(tagName, attrName) {
|
||||
return includes(GLOBAL_ATTRS, attrName) || (TAG_ATTRS[tagName] && includes(TAG_ATTRS[tagName], attrName));
|
||||
}
|
||||
|
||||
function unwrapNode(node) {
|
||||
const parent = node.parentNode;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
while (node.firstChild) {
|
||||
parent.insertBefore(node.firstChild, node);
|
||||
}
|
||||
parent.removeChild(node);
|
||||
}
|
||||
|
||||
function sanitizeElement(node, allowedTags) {
|
||||
const tagName = node.nodeName.toLowerCase();
|
||||
if (includes(DANGEROUS_TAGS, tagName)) {
|
||||
node.parentNode.removeChild(node);
|
||||
return;
|
||||
}
|
||||
if (!includes(allowedTags, tagName)) {
|
||||
sanitizeChildren(node, allowedTags);
|
||||
unwrapNode(node);
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(node.attributes).forEach((attr) => {
|
||||
const attrName = attr.name.toLowerCase();
|
||||
const attrValue = attr.value;
|
||||
if (/^on/i.test(attrName) || !isAllowedAttr(tagName, attrName)) {
|
||||
node.removeAttribute(attr.name);
|
||||
return;
|
||||
}
|
||||
if ((attrName === 'href' || attrName === 'src') && !isSafeUrl(attrValue, tagName === 'img')) {
|
||||
node.removeAttribute(attr.name);
|
||||
return;
|
||||
}
|
||||
if (attrName === 'style') {
|
||||
const safeStyle = sanitizeStyle(attrValue);
|
||||
if (safeStyle) {
|
||||
node.setAttribute('style', safeStyle);
|
||||
} else {
|
||||
node.removeAttribute('style');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (attrName === 'class') {
|
||||
const safeClass = sanitizeClass(attrValue);
|
||||
if (safeClass) {
|
||||
node.setAttribute('class', safeClass);
|
||||
} else {
|
||||
node.removeAttribute('class');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (tagName === 'a' && attrName === 'target') {
|
||||
if (attrValue === '_blank') {
|
||||
node.setAttribute('rel', 'noopener noreferrer');
|
||||
} else {
|
||||
node.removeAttribute(attr.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
sanitizeChildren(node, allowedTags);
|
||||
}
|
||||
|
||||
function sanitizeChildren(parent, allowedTags) {
|
||||
Array.from(parent.childNodes).forEach((node) => {
|
||||
if (node.nodeType === 1) {
|
||||
sanitizeElement(node, allowedTags);
|
||||
} else if (node.nodeType !== 3) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sanitizeHtml(html, allowedTags) {
|
||||
if (html === null || html === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (typeof document === 'undefined') {
|
||||
return escapeHtml(html);
|
||||
}
|
||||
const template = document.createElement('template');
|
||||
template.innerHTML = String(html);
|
||||
sanitizeChildren(template.content || template, allowedTags);
|
||||
return template.innerHTML;
|
||||
}
|
||||
|
||||
export function escapeHtml(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return '';
|
||||
}
|
||||
return String(value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
export function sanitizeRichText(html) {
|
||||
return sanitizeHtml(html, RICH_TEXT_ALLOWED_TAGS);
|
||||
}
|
||||
|
||||
export function sanitizeDisplayHtml(html) {
|
||||
return sanitizeHtml(html, DISPLAY_ALLOWED_TAGS);
|
||||
}
|
||||
|
||||
export function hasXssRisk(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return false;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((item) => hasXssRisk(item));
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
return Object.keys(value).some((key) => hasXssRisk(value[key]));
|
||||
}
|
||||
const text = normalizeForScan(value);
|
||||
return XSS_RISK_PATTERNS.some((pattern) => pattern.test(text));
|
||||
}
|
||||
@@ -211,7 +211,7 @@
|
||||
<span style="margin-left: 20px">{{ notice.createTime }}</span>
|
||||
</div>
|
||||
<div v-loading="loading" class="content">
|
||||
<div v-html="notice.noticeContent"></div>
|
||||
<safe-html :html="notice.noticeContent" mode="richText"></safe-html>
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="closeDetail">{{ $t('close') }}</el-button>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('speaker.clientDetails.index.893021-14')" align="center" prop="authorizedGrantTypes">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="formatGrantTypesDisplay(scope.row.authorizedGrantTypes)"></div>
|
||||
<safe-html :html="formatGrantTypesDisplay(scope.row.authorizedGrantTypes)"></safe-html>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('speaker.clientDetails.index.893021-15')" align="center" prop="webServerRedirectUri" min-width="130" />
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<el-table-column :label="$t('device.device-log.798283-2')" align="center" prop="identify" />
|
||||
<el-table-column :label="$t('device.device-log.798283-15')" align="left" header-align="center" prop="logValue">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="formatValueDisplay(scope.row)"></div>
|
||||
<safe-html :html="formatValueDisplay(scope.row)"></safe-html>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
<el-table-column :label="$t('device.device-timer.433369-7')" align="center" prop="jobName" :show-overflow-tooltip="true" />
|
||||
<el-table-column :label="$t('device.device-timer.433369-8')" align="center" prop="cronText">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="formatCronDisplay(scope.row)"></div>
|
||||
<safe-html :html="formatCronDisplay(scope.row)"></safe-html>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('device.device-timer.433369-9')" align="center" prop="cronExpression" :show-overflow-tooltip="true" />
|
||||
<el-table-column :label="$t('device.device-timer.433369-10')" align="left" prop="actions" :show-overflow-tooltip="true">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="formatActionsDisplay(scope.row.actions)" style="overflow: hidden; white-space: nowrap"></div>
|
||||
<safe-html :html="formatActionsDisplay(scope.row.actions)" style="overflow: hidden; white-space: nowrap"></safe-html>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -222,8 +222,8 @@
|
||||
|
||||
<el-col :span="24">
|
||||
<el-form-item :label="$t('device.device-timer.433369-52')">
|
||||
<div v-html="formatActionsDisplay(form.actions)"
|
||||
style="border: 1px solid #ddd; padding: 10px; border-radius: 5px; width: 465px"></div>
|
||||
<safe-html :html="formatActionsDisplay(form.actions)"
|
||||
style="border: 1px solid #ddd; padding: 10px; border-radius: 5px; width: 465px"></safe-html>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
@@ -177,6 +177,24 @@
|
||||
|
||||
<script>
|
||||
import { listLog, getLog, delLog, addLog, updateLog } from '@/api/iot/log';
|
||||
import { hasXssRisk } from '@/utils/security';
|
||||
|
||||
const XSS_CHECK_FIELDS = [
|
||||
'logName',
|
||||
'logLevel',
|
||||
'deviceId',
|
||||
'deviceName',
|
||||
'userId',
|
||||
'userName',
|
||||
'tenantId',
|
||||
'tenantName',
|
||||
'triggerSource',
|
||||
'isAlert',
|
||||
'remark',
|
||||
'logValue',
|
||||
'istop',
|
||||
'ismonitor',
|
||||
];
|
||||
|
||||
export default {
|
||||
name: 'Log',
|
||||
@@ -367,6 +385,10 @@ export default {
|
||||
submitForm() {
|
||||
this.$refs['form'].validate((valid) => {
|
||||
if (valid) {
|
||||
if (this.hasLogXssRisk()) {
|
||||
this.$modal.msgError('日志内容包含疑似XSS脚本特征,请修改后再提交');
|
||||
return;
|
||||
}
|
||||
if (this.form.deviceLogId != null) {
|
||||
updateLog(this.form).then((response) => {
|
||||
this.$modal.msgSuccess(this.$t('iot.group.index.637432-24'));
|
||||
@@ -383,6 +405,9 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
hasLogXssRisk() {
|
||||
return XSS_CHECK_FIELDS.some((field) => hasXssRisk(this.form[field]));
|
||||
},
|
||||
/** 删除按钮操作 */
|
||||
handleDelete(row) {
|
||||
const deviceLogIds = row.deviceLogId || this.ids;
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
<span style="margin-left: 20px">{{ form.createTime }}</span>
|
||||
</div>
|
||||
<div v-loading="loadingDetail" class="content">
|
||||
<div v-html="form.content"></div>
|
||||
<safe-html :html="form.content" mode="richText"></safe-html>
|
||||
</div>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button type="primary" @click="closeDetail">{{ $t('device.device-edit.148398-57') }}</el-button>
|
||||
@@ -182,6 +182,7 @@
|
||||
import { listNews, getNews, delNews, addNews, updateNews } from '@/api/iot/news';
|
||||
import { listShortNewsCategory } from '@/api/iot/newsCategory';
|
||||
import imageUpload from '../../../components/ImageUpload/index';
|
||||
import { sanitizeRichText } from '@/utils/security';
|
||||
|
||||
export default {
|
||||
name: 'News',
|
||||
@@ -363,6 +364,7 @@ export default {
|
||||
}
|
||||
this.$refs['form'].validate((valid) => {
|
||||
if (valid) {
|
||||
this.form.content = sanitizeRichText(this.form.content);
|
||||
if (this.form.newsId != null) {
|
||||
updateNews(this.form).then((response) => {
|
||||
this.$modal.msgSuccess(this.$t('iot.group.index.637432-24'));
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('product.product-things-model.142341-18')" align="left" header-align="center" prop="specs" min-width="150" class-name="specsColor">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="formatSpecsDisplay(scope.row.specs)"></div>
|
||||
<safe-html :html="formatSpecsDisplay(scope.row.specs)"></safe-html>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('product.product-things-model.142341-19')" align="center" prop="formula" />
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('template.index.891112-15')" align="left" header-align="center" prop="specs" min-width="150" class-name="specsColor">
|
||||
<template slot-scope="scope">
|
||||
<div v-html="formatSpecsDisplay(scope.row.specs)"></div>
|
||||
<safe-html :html="formatSpecsDisplay(scope.row.specs)"></safe-html>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('template.index.891112-16')" align="center" prop="modelOrder" width="80" />
|
||||
|
||||
@@ -107,6 +107,7 @@
|
||||
|
||||
<script>
|
||||
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from '@/api/system/notice';
|
||||
import { sanitizeRichText } from '@/utils/security';
|
||||
|
||||
export default {
|
||||
name: 'Notice',
|
||||
@@ -213,6 +214,7 @@ export default {
|
||||
submitForm: function () {
|
||||
this.$refs['form'].validate((valid) => {
|
||||
if (valid) {
|
||||
this.form.noticeContent = sanitizeRichText(this.form.noticeContent);
|
||||
if (this.form.noticeId != undefined) {
|
||||
updateNotice(this.form).then((response) => {
|
||||
this.$modal.msgSuccess(this.$t('updateSuccess'));
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<el-tabs v-model="preview.activeName">
|
||||
<el-tab-pane v-for="(value, key) in preview.data" :label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))" :name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))" :key="key">
|
||||
<el-link :underline="false" icon="el-icon-document-copy" v-clipboard:copy="value" v-clipboard:success="clipboardSuccess" style="float: right">{{ $t('device.device-edit.148398-55') }}</el-link>
|
||||
<pre><code class="hljs" v-html="highlightedCode(value, key)"></code></pre>
|
||||
<pre><code class="hljs"><safe-html :html="highlightedCode(value, key)" inline></safe-html></code></pre>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
|
||||
Reference in New Issue
Block a user