diff --git a/backend/src/main/java/io/dataease/commons/utils/DeFileUtils.java b/backend/src/main/java/io/dataease/commons/utils/DeFileUtils.java index 6adc42e9be..2dcee5fa41 100644 --- a/backend/src/main/java/io/dataease/commons/utils/DeFileUtils.java +++ b/backend/src/main/java/io/dataease/commons/utils/DeFileUtils.java @@ -1,5 +1,7 @@ package io.dataease.commons.utils; +import io.dataease.commons.exception.DEException; +import org.apache.commons.lang3.StringUtils; import org.springframework.web.multipart.MultipartFile; import java.io.*; @@ -41,6 +43,17 @@ public class DeFileUtils { if (dir.exists()) return ; dir.mkdirs(); } + + public static void validateFile(MultipartFile file) { + String name = getFileNameNoEx(file.getOriginalFilename()); + if (StringUtils.contains(name, "./")) { + DEException.throwException("file path invalid"); + } + String suffix = getExtensionName(file.getOriginalFilename()); + if (!StringUtils.equalsIgnoreCase(suffix, "zip")) { + DEException.throwException("please upload valid zip file"); + } + } /** * 将文件名解析成文件的上传路径 */ diff --git a/backend/src/main/java/io/dataease/commons/wrapper/XssAndSqlHttpServletRequestWrapper.java b/backend/src/main/java/io/dataease/commons/wrapper/XssAndSqlHttpServletRequestWrapper.java index 672f89d46a..0686f48b8f 100644 --- a/backend/src/main/java/io/dataease/commons/wrapper/XssAndSqlHttpServletRequestWrapper.java +++ b/backend/src/main/java/io/dataease/commons/wrapper/XssAndSqlHttpServletRequestWrapper.java @@ -1,18 +1,6 @@ package io.dataease.commons.wrapper; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.servlet.ReadListener; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; - import com.google.gson.Gson; import io.dataease.commons.holder.ThreadLocalContextHolder; import io.dataease.commons.utils.CommonBeanFactory; @@ -21,16 +9,30 @@ import org.apache.commons.lang3.StringUtils; import org.springframework.core.env.Environment; import org.springframework.util.StreamUtils; +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrapper { private static Gson gson = new Gson(); + private static final String defaultWhiteList = "/dataset/table/sqlPreview,/dataset/table/update,/dataset/field/multFieldValues,/dataset/field/linkMultFieldValues"; + HttpServletRequest orgRequest = null; private Map parameterMap; private final byte[] body; //用于保存读取body中数据 - public XssAndSqlHttpServletRequestWrapper(HttpServletRequest request) throws IOException{ + public XssAndSqlHttpServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); orgRequest = request; parameterMap = request.getParameterMap(); @@ -38,6 +40,7 @@ public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrappe } // 重写几个HttpServletRequestWrapper中的方法 + /** * 获取所有参数名 * @@ -159,7 +162,6 @@ public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrappe } /** - * * 防止xss跨脚本攻击(替换,根据实际情况调整) */ @@ -208,9 +210,9 @@ public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrappe return value; } - public static boolean checkSqlInjection(Object obj){ + public static boolean checkSqlInjection(Object obj) { HttpServletRequest request = ServletUtils.request(); - String url = request.getRequestURI().toString(); + String url = request.getRequestURI(); if (null == obj) return false; if (StringUtils.isEmpty(obj.toString())) return false; @@ -219,14 +221,14 @@ public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrappe if (StringUtils.isEmpty(orders)) return false; - String whiteLists = CommonBeanFactory.getBean(Environment.class).getProperty("dataease.sqlinjection.whitelists", String.class, null); + String whiteLists = CommonBeanFactory.getBean(Environment.class).getProperty("dataease.sqlinjection.whitelists", String.class, defaultWhiteList); if (StringUtils.isNotEmpty(whiteLists)) { // 命中白名单 无需检测sql注入 if (Arrays.stream(whiteLists.split(",")).anyMatch(item -> url.indexOf(item) != -1)) return false; } - Pattern pattern= Pattern.compile("(.*\\=.*\\-\\-.*)|(.*(\\+).*)|(.*\\w+(%|\\$|#|&)\\w+.*)|(.*\\|\\|.*)|(.*\\s+(and|or)\\s+.*)" + + Pattern pattern = Pattern.compile("(.*\\=.*\\-\\-.*)|(.*(\\+).*)|(.*\\w+(%|\\$|#|&)\\w+.*)|(.*\\|\\|.*)|(.*\\s+(and|or)\\s+.*)" + "|(.*\\b(select|update|union|and|or|delete|insert|trancate|char|into|substr|ascii|declare|exec|count|master|into|drop|execute|sleep|extractvalue|updatexml|substring|database|concat|rand|gtid_subset)\\b.*)"); - Matcher matcher=pattern.matcher(orders.toLowerCase()); + Matcher matcher = pattern.matcher(orders.toLowerCase()); return matcher.find(); } @@ -236,7 +238,7 @@ public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrappe if (value != null) { boolean b = checkSqlInjection(value); - if(b) { + if (b) { ThreadLocalContextHolder.setData("包含SQL注入的参数,请检查参数!"); return true; } @@ -320,7 +322,7 @@ public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrappe return true; } } else if ((submitValues instanceof String[])) { - for (String submitValue : (String[])submitValues){ + for (String submitValue : (String[]) submitValues) { if (checkXSSAndSql(submitValue)) { return true; } @@ -332,7 +334,7 @@ public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrappe private static String orders(String json) { if (StringUtils.isEmpty(json)) return null; - try{ + try { Map map = new Gson().fromJson(json, Map.class); Object orders = map.get("orders"); @@ -345,7 +347,7 @@ public class XssAndSqlHttpServletRequestWrapper extends HttpServletRequestWrappe return sort.toString(); } return null; - }catch (Exception e) { + } catch (Exception e) { return null; } diff --git a/backend/src/main/java/io/dataease/controller/sys/SysPluginController.java b/backend/src/main/java/io/dataease/controller/sys/SysPluginController.java index 7f3b3e669f..d8e32122c4 100644 --- a/backend/src/main/java/io/dataease/controller/sys/SysPluginController.java +++ b/backend/src/main/java/io/dataease/controller/sys/SysPluginController.java @@ -3,10 +3,11 @@ package io.dataease.controller.sys; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import io.dataease.auth.annotation.SqlInjectValidator; -import io.dataease.plugins.common.base.domain.MyPlugin; +import io.dataease.commons.utils.DeFileUtils; import io.dataease.commons.utils.PageUtils; import io.dataease.commons.utils.Pager; import io.dataease.controller.sys.base.BaseGridRequest; +import io.dataease.plugins.common.base.domain.MyPlugin; import io.dataease.service.sys.PluginService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -41,6 +42,7 @@ public class SysPluginController { @PostMapping("upload") @RequiresPermissions("plugin:upload") public Map localUpload(@RequestParam("file") MultipartFile file) throws Exception { + DeFileUtils.validateFile(file); return pluginService.localInstall(file); } @@ -54,7 +56,8 @@ public class SysPluginController { @ApiOperation("更新插件") @PostMapping("/update/{pluginId}") @RequiresPermissions("plugin:upload") - public Map update(@PathVariable("pluginId") Long pluginId, @RequestParam("file") MultipartFile file) throws Exception{ + public Map update(@PathVariable("pluginId") Long pluginId, @RequestParam("file") MultipartFile file) throws Exception { + DeFileUtils.validateFile(file); if (pluginService.uninstall(pluginId)) { return pluginService.localInstall(file); } diff --git a/backend/src/main/java/io/dataease/service/chart/ChartViewService.java b/backend/src/main/java/io/dataease/service/chart/ChartViewService.java index 6077bbe9eb..1b7384fd61 100644 --- a/backend/src/main/java/io/dataease/service/chart/ChartViewService.java +++ b/backend/src/main/java/io/dataease/service/chart/ChartViewService.java @@ -33,6 +33,7 @@ import io.dataease.plugins.common.base.mapper.ChartViewMapper; import io.dataease.plugins.common.base.mapper.DatasetTableFieldMapper; import io.dataease.plugins.common.base.mapper.PanelViewMapper; import io.dataease.plugins.common.constants.DatasetType; +import io.dataease.plugins.common.constants.DatasourceTypes; import io.dataease.plugins.common.constants.datasource.SQLConstants; import io.dataease.plugins.common.dto.chart.ChartCustomFilterItemDTO; import io.dataease.plugins.common.dto.chart.ChartFieldCompareDTO; @@ -611,7 +612,7 @@ public class ChartViewService { xAxis.addAll(xAxisExt); } List yAxis = gson.fromJson(view.getYAxis(), tokenType); - if (StringUtils.equalsAnyIgnoreCase(view.getType(), "chart-mix","bidirectional-bar")) { + if (StringUtils.equalsAnyIgnoreCase(view.getType(), "chart-mix", "bidirectional-bar")) { List yAxisExt = gson.fromJson(view.getYAxisExt(), tokenType); yAxis.addAll(yAxisExt); } @@ -1126,7 +1127,7 @@ public class ChartViewService { datasourceRequest.setTotalPageFlag(false); data = datasourceProvider.getData(datasourceRequest); if (CollectionUtils.isNotEmpty(assistFields)) { - datasourceAssistRequest.setQuery(assistSQL(datasourceRequest.getQuery(), assistFields)); + datasourceAssistRequest.setQuery(assistSQL(datasourceRequest.getQuery(), assistFields, ds)); logger.info(datasourceAssistRequest.getQuery()); assistData = datasourceProvider.getData(datasourceAssistRequest); } @@ -1158,7 +1159,7 @@ public class ChartViewService { } } if (CollectionUtils.isNotEmpty(assistFields)) { - datasourceAssistRequest.setQuery(assistSQL(datasourceRequest.getQuery(), assistFields)); + datasourceAssistRequest.setQuery(assistSQL(datasourceRequest.getQuery(), assistFields, ds)); logger.info(datasourceAssistRequest.getQuery()); assistData = datasourceProvider.getData(datasourceAssistRequest); } @@ -1405,14 +1406,15 @@ public class ChartViewService { return res; } - public String assistSQL(String sql, List assistFields) { + public String assistSQL(String sql, List assistFields, Datasource ds) { + DatasourceTypes datasourceType = DatasourceTypes.valueOf(ds.getType()); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < assistFields.size(); i++) { ChartViewFieldDTO dto = assistFields.get(i); if (i == (assistFields.size() - 1)) { - stringBuilder.append(dto.getSummary() + "(" + dto.getOriginName() + ")"); + stringBuilder.append(dto.getSummary() + "(" + datasourceType.getKeywordPrefix() + dto.getOriginName() + datasourceType.getKeywordSuffix() + ")"); } else { - stringBuilder.append(dto.getSummary() + "(" + dto.getOriginName() + "),"); + stringBuilder.append(dto.getSummary() + "(" + datasourceType.getKeywordPrefix() + dto.getOriginName() + datasourceType.getKeywordSuffix() + "),"); } } return "SELECT " + stringBuilder + " FROM (" + sql + ") tmp"; diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index e2095991b5..fb50ef16d8 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -68,7 +68,7 @@ spring.cache.ehcache.config=classpath:/ehcache/ehcache.xml pagehelper.PageRowBounds=true #excel\u7B49\u7528\u6237\u4E0A\u4F20\u6587\u4EF6\u8DEF\u5F84 upload.file.path=/opt/dataease/data/kettle/ -dataease.sqlinjection.whitelists=/dataset/table/sqlPreview,/dataset/table/update,/dataset/field/multFieldValues,/dataset/field/linkMultFieldValues +#dataease.sqlinjection.whitelists=/dataset/table/sqlPreview,/dataset/table/update,/dataset/field/multFieldValues,/dataset/field/linkMultFieldValues #\u5F00\u542F\u538B\u7F29 \u63D0\u9AD8\u54CD\u5E94\u901F\u5EA6 \u51CF\u5C11\u5E26\u5BBD\u538B\u529B server.compression.enabled=true server.compression.mime-types=application/javascript,text/css,application/json,application/xml,text/html,text/xml,text/plain diff --git a/backend/src/main/resources/db/migration/V56__1.18.9.sql b/backend/src/main/resources/db/migration/V56__1.18.9.sql new file mode 100644 index 0000000000..3d6018d62f --- /dev/null +++ b/backend/src/main/resources/db/migration/V56__1.18.9.sql @@ -0,0 +1,4 @@ +UPDATE `my_plugin` +SET `version` = '1.18.9' +where `plugin_id` > 0 + and `version` = '1.18.8'; diff --git a/frontend/package.json b/frontend/package.json index a7449158f9..ba27c63f0d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "dataease", - "version": "1.18.8", + "version": "1.18.9", "description": "dataease front", "private": true, "scripts": { diff --git a/frontend/src/components/canvas/components/editor/ComponentWrapper.vue b/frontend/src/components/canvas/components/editor/ComponentWrapper.vue index 4b9dd62481..58f49bca1c 100644 --- a/frontend/src/components/canvas/components/editor/ComponentWrapper.vue +++ b/frontend/src/components/canvas/components/editor/ComponentWrapper.vue @@ -151,13 +151,18 @@ export default { data() { return { previewVisible: false, - chart: null, seriesIdMap: { id: '' } } }, computed: { + chart() { + if (this.config.propValue?.viewId) { + return JSON.parse(this.panelViewDetailsInfo[this.config.propValue.viewId]) + } + return null + }, componentCanvasId() { if (this.config.type === 'view') { return 'user-view-' + this.config.propValue.viewId @@ -216,7 +221,8 @@ export default { 'mobileLayoutStatus', 'curComponent', 'previewCanvasScale', - 'componentGap' + 'componentGap', + 'panelViewDetailsInfo' ]) }, mounted() { @@ -286,8 +292,7 @@ export default { } } else { return { - ... - getStyle(style, ['top', 'left', 'width', 'height', 'rotate']), + ...getStyle(style, ['top', 'left', 'width', 'height', 'rotate']), position: 'relative' } } diff --git a/frontend/src/components/canvas/components/editor/EditBar.vue b/frontend/src/components/canvas/components/editor/EditBar.vue index eddb002f73..a185a38f08 100644 --- a/frontend/src/components/canvas/components/editor/EditBar.vue +++ b/frontend/src/components/canvas/components/editor/EditBar.vue @@ -294,7 +294,7 @@ export default { return this.curComponent.type === 'view' && this.terminal === 'pc' && this.curComponent.propValue.innerType && this.curComponent.propValue.innerType !== 'richTextView' }, exportExcelShow() { - return this.detailsShow && hasDataPermission('export', this.$store.state.panel.panelInfo.privileges) + return this.detailsShow && hasDataPermission('export', this.$store.state.panel.panelInfo.privileges) && this.chart }, enlargeShow() { return this.curComponent.type === 'view' && this.curComponent.propValue.innerType && this.curComponent.propValue.innerType !== 'richTextView' && !this.curComponent.propValue.innerType.includes('table') diff --git a/frontend/src/components/canvas/utils/utils.js b/frontend/src/components/canvas/utils/utils.js index 84157b5295..bd262fc4d1 100644 --- a/frontend/src/components/canvas/utils/utils.js +++ b/frontend/src/components/canvas/utils/utils.js @@ -429,6 +429,9 @@ export function getCacheTree(treeName) { } export function exportExcelDownload(chart, snapshot, width, height, loadingWrapper, callBack) { + if (!chart.data?.data?.length) { + return + } const fields = JSON.parse(JSON.stringify(chart.data.fields)) const tableRow = JSON.parse(JSON.stringify(chart.data.tableRow)) const excelHeader = fields.map(item => item.name) diff --git a/frontend/src/icons/svg/file-excel.svg b/frontend/src/icons/svg/file-excel.svg new file mode 100644 index 0000000000..253306726f --- /dev/null +++ b/frontend/src/icons/svg/file-excel.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/lang/en.js b/frontend/src/lang/en.js index e78404581a..bf2ffbffa2 100644 --- a/frontend/src/lang/en.js +++ b/frontend/src/lang/en.js @@ -935,6 +935,8 @@ export default { password_input_error: 'Original password input error' }, chart: { + empty_hide: 'hide empty', + hide: 'hide', chart_refresh_tips: 'View refresh setting takes precedence over panel refresh setting', '1-trend': 'trend', '2-state': 'State', diff --git a/frontend/src/lang/tw.js b/frontend/src/lang/tw.js index 9c09e989fa..05355e1942 100644 --- a/frontend/src/lang/tw.js +++ b/frontend/src/lang/tw.js @@ -934,6 +934,8 @@ export default { password_input_error: '原始密碼輸入錯誤' }, chart: { + empty_hide: '隱藏空值', + hide: '隱藏', chart_refresh_tips: '視圖刷新設置優先於儀表板刷新設置', '1-trend': '趨勢', '2-state': '狀態', diff --git a/frontend/src/lang/zh.js b/frontend/src/lang/zh.js index fc55889561..7bb7c43200 100644 --- a/frontend/src/lang/zh.js +++ b/frontend/src/lang/zh.js @@ -933,6 +933,8 @@ export default { password_input_error: '原始密码输入错误' }, chart: { + empty_hide: '隐藏空值', + hide: '隐藏', chart_refresh_tips: '视图刷新设置优先于仪表板刷新设置', '1-trend': '趋势', '2-state': '状态', diff --git a/frontend/src/views/chart/chart/map/map.js b/frontend/src/views/chart/chart/map/map.js index 1d534f9a9e..5433ca8441 100644 --- a/frontend/src/views/chart/chart/map/map.js +++ b/frontend/src/views/chart/chart/map/map.js @@ -72,9 +72,13 @@ export function baseMapOption(chart_option, geoJson, chart, themeStyle, curAreaC const reg = new RegExp('\n', 'g') const text = tooltip.formatter.replace(reg, '
') tooltip.formatter = params => { + const val = params.value + if (tooltip.emptyHide && (val === null || typeof val === 'undefined' || isNaN(val))) { + return '' + } const a = params.seriesName const b = params.name - const c = params.value ?? '' + const c = (val === null || typeof val === 'undefined' || isNaN(val)) ? '' : val return text.replace(new RegExp('{a}', 'g'), a).replace(new RegExp('{b}', 'g'), b).replace(new RegExp('{c}', 'g'), c) } chart_option.tooltip = tooltip diff --git a/frontend/src/views/chart/chart/util.js b/frontend/src/views/chart/chart/util.js index 52ad0e780b..2d06070bdd 100644 --- a/frontend/src/views/chart/chart/util.js +++ b/frontend/src/views/chart/chart/util.js @@ -3323,6 +3323,7 @@ export const TYPE_CONFIGS = [ ], 'tooltip-selector': [ 'show', + 'emptyHide', 'textStyle', 'formatter' ], diff --git a/frontend/src/views/chart/components/shapeAttr/TooltipSelector.vue b/frontend/src/views/chart/components/shapeAttr/TooltipSelector.vue index 618d32e305..b04e740df2 100644 --- a/frontend/src/views/chart/components/shapeAttr/TooltipSelector.vue +++ b/frontend/src/views/chart/components/shapeAttr/TooltipSelector.vue @@ -18,6 +18,16 @@ >{{ $t('chart.show') }}
+ + {{ $t('chart.hide') }} + - 1.18.8 + 1.18.9 dataease