diff --git a/.codecov.yml b/.codecov.yml
index cc2b01f97..3332c1300 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -4,7 +4,7 @@ coverage:
precision: 2
status:
- project: yes
+ project: true
patch: no
changes: no
diff --git a/.eslintrc.js b/.eslintrc.js
index 3adf921d1..a49965c7d 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -55,6 +55,7 @@ module.exports = {
$fileExtName: true,
$gotoSection: true,
$createUploader: true,
+ $initUploader: true,
$cleanMenu: true,
$cleanMap: true,
$pages: true,
diff --git a/@rbv b/@rbv
index 7b0d1b946..0aa958543 160000
--- a/@rbv
+++ b/@rbv
@@ -1 +1 @@
-Subproject commit 7b0d1b946a9c4ba5a832a57dc3db17606c6e912f
+Subproject commit 0aa958543b1eb60bbfe69bc3c9e689c5d0c224d6
diff --git a/crowdin.yml b/crowdin.yml
deleted file mode 100644
index e65118f4d..000000000
--- a/crowdin.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-commit_message: '[Crowdin] Update translations'
-files:
- - source: '/src/main/resources/i18n/language.zh_CN.json'
- translation: '/src/main/resources/i18n/language.%locale%.json'
- languages_mapping:
- locale:
- 'zh-CN': 'zh_CN'
- 'en-US': 'en'
diff --git a/src/main/java/com/rebuild/core/support/CsrfToken.java b/src/main/java/com/rebuild/core/support/CsrfToken.java
new file mode 100644
index 000000000..693c4d629
--- /dev/null
+++ b/src/main/java/com/rebuild/core/support/CsrfToken.java
@@ -0,0 +1,67 @@
+/*
+Copyright (c) REBUILD and/or its owners. All rights reserved.
+
+rebuild is dual-licensed under commercial and open source licenses (GPLv3).
+See LICENSE and COMMERCIAL in the project root for license information.
+*/
+
+package com.rebuild.core.support;
+
+import cn.devezhao.commons.CodecUtils;
+import com.rebuild.core.Application;
+import com.rebuild.core.cache.CommonsCache;
+import com.rebuild.utils.AppUtils;
+import org.apache.commons.lang.StringUtils;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * @author devezhao
+ * @since 2020/12/15
+ */
+public class CsrfToken {
+
+ // Token 存储前缀
+ private static final String TOKEN_PREFIX = "RBCSRF.";
+
+ /**
+ * 生成并存储 Token
+ *
+ * @return
+ */
+ public static String generate() {
+ String token = CodecUtils.randomCode(60);
+ Application.getCommonsCache().putx(TOKEN_PREFIX + token,
+ System.currentTimeMillis(), CommonsCache.TS_HOUR * 2);
+ return token;
+ }
+
+ /**
+ * 验证 Token
+ *
+ * @param token
+ * @param destroy
+ * @return
+ */
+ public static boolean verify(String token, boolean destroy) {
+ if (StringUtils.isBlank(token)) return false;
+
+ token = TOKEN_PREFIX + token;
+ Object exists = Application.getCommonsCache().getx(token);
+ if (exists != null && destroy) {
+ Application.getCommonsCache().evict(token);
+ }
+ return exists != null;
+ }
+
+ /**
+ * @param request
+ * @param destroy
+ * @return
+ */
+ public static boolean verify(HttpServletRequest request, boolean destroy) {
+ String token = request.getHeader("X-Csrf-Token");
+ if (token == null) token = request.getParameter("_token");
+ return verify(token, destroy);
+ }
+}
diff --git a/src/main/java/com/rebuild/utils/AppUtils.java b/src/main/java/com/rebuild/utils/AppUtils.java
index 081aa7609..7720b95ae 100644
--- a/src/main/java/com/rebuild/utils/AppUtils.java
+++ b/src/main/java/com/rebuild/utils/AppUtils.java
@@ -187,10 +187,10 @@ public class AppUtils {
public static MimeType parseMimeType(HttpServletRequest request) {
try {
String acceptType = request.getHeader("Accept");
- if (acceptType == null) acceptType = request.getContentType();
+ if (acceptType == null || "*/*".equals(acceptType)) acceptType = request.getContentType();
// Via Spider?
- if (acceptType == null) return MimeTypeUtils.TEXT_HTML;
+ if (StringUtils.isBlank(acceptType)) return MimeTypeUtils.TEXT_HTML;
acceptType = acceptType.split("[,;]")[0];
// Accpet ALL?
diff --git a/src/main/java/com/rebuild/web/RebuildWebInterceptor.java b/src/main/java/com/rebuild/web/RebuildWebInterceptor.java
index 646cbc021..e92608dec 100644
--- a/src/main/java/com/rebuild/web/RebuildWebInterceptor.java
+++ b/src/main/java/com/rebuild/web/RebuildWebInterceptor.java
@@ -18,6 +18,7 @@ import com.rebuild.core.UserContextHolder;
import com.rebuild.core.cache.CommonsCache;
import com.rebuild.core.privileges.bizz.ZeroEntry;
import com.rebuild.core.support.ConfigurationItem;
+import com.rebuild.core.support.CsrfToken;
import com.rebuild.core.support.License;
import com.rebuild.core.support.RebuildConfiguration;
import com.rebuild.core.support.setup.InstallState;
@@ -28,8 +29,8 @@ import org.apache.commons.lang.StringUtils;
import org.springframework.core.NamedThreadLocal;
import org.springframework.http.HttpStatus;
import org.springframework.util.MimeTypeUtils;
+import org.springframework.web.servlet.AsyncHandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
-import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -45,7 +46,7 @@ import java.io.IOException;
* @since 2.0
*/
@Slf4j
-public class RebuildWebInterceptor extends HandlerInterceptorAdapter implements InstallState {
+public class RebuildWebInterceptor implements AsyncHandlerInterceptor, InstallState {
private static final ThreadLocal REQUEST_ENTRY = new NamedThreadLocal<>("RequestEntry");
@@ -143,6 +144,11 @@ public class RebuildWebInterceptor extends HandlerInterceptorAdapter implements
}
} else if (!isIgnoreAuth(requestUri)) {
+ // 外部表单特殊处理(媒体字段上传/预览)
+ if (requestUri.contains("/filex/") && CsrfToken.verify(request, false)) {
+ return true;
+ }
+
log.warn("Unauthorized access {} via {}",
RebuildWebConfigurer.getRequestUrls(request), ServletUtils.getRemoteAddr(request));
@@ -160,7 +166,7 @@ public class RebuildWebInterceptor extends HandlerInterceptorAdapter implements
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
- super.postHandle(request, response, handler, modelAndView);
+ // Notings
}
@Override
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 9c5ba5e52..af7357a64 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -20,12 +20,13 @@ server:
# SPRING
spring:
- mvc:
- static-path-pattern: /assets/**
- resources:
- static-locations: classpath:/web/assets
servlet:
multipart.enabled: false
+ web:
+ resources:
+ static-locations: classpath:/web/assets
+ mvc:
+ static-path-pattern: /assets/**
thymeleaf:
prefix: classpath:/web
cache: true
@@ -37,3 +38,4 @@ spring:
size: 5
main:
banner-mode: off
+
diff --git a/src/main/resources/web/assets/css/rb-base.css b/src/main/resources/web/assets/css/rb-base.css
index c68d91d85..240b0a04d 100644
--- a/src/main/resources/web/assets/css/rb-base.css
+++ b/src/main/resources/web/assets/css/rb-base.css
@@ -20135,12 +20135,11 @@ label {
z-index: -1;
}
-.inputfile + label {
+.inputfile + label, .inputfile-label {
padding: 0 12px;
font-size: 1rem;
line-height: 30px;
- border: 1px solid transparent;
- border-color: #d5d8de;
+ border: 1px solid #d5d8de;
border-radius: 2px;
display: inline-block;
cursor: pointer;
diff --git a/src/main/resources/web/assets/css/rb-page.css b/src/main/resources/web/assets/css/rb-page.css
index fffd288a4..b78990fb5 100644
--- a/src/main/resources/web/assets/css/rb-page.css
+++ b/src/main/resources/web/assets/css/rb-page.css
@@ -1014,7 +1014,7 @@ a {
border: 0 none;
color: #404040;
border-left: 2px solid #ccc;
- width: 205px;
+ width: 239px;
border-radius: 2px;
max-height: 38px;
cursor: default;
diff --git a/src/main/resources/web/assets/js/rb-page.js b/src/main/resources/web/assets/js/rb-page.js
index b020eae5e..5aa5f4727 100644
--- a/src/main/resources/web/assets/js/rb-page.js
+++ b/src/main/resources/web/assets/js/rb-page.js
@@ -441,13 +441,15 @@ var $createUploader = function (input, next, complete, error) {
var local = input.data('local')
if (!input.attr('data-maxsize')) input.attr('data-maxsize', 1024 * 1024 * 100) // default 100M
+ var useToken = rb.csrfToken ? ('&_token=' + rb.csrfToken) : ''
+
if (window.qiniu && rb.storageUrl && !local) {
input.on('change', function () {
var file = this.files[0]
if (!file) return
var putExtra = imgOnly ? { mimeType: ['image/png', 'image/jpeg', 'image/gif', 'image/bmp'] } : null
- $.get('/filex/qiniu/upload-keys?file=' + $encode(file.name), function (res) {
+ $.get('/filex/qiniu/upload-keys?file=' + $encode(file.name) + useToken, function (res) {
var o = qiniu.upload(file, res.data.key, res.data.token, putExtra)
o.subscribe({
next: function (res) {
@@ -466,7 +468,7 @@ var $createUploader = function (input, next, complete, error) {
return false
},
complete: function (res) {
- if (file.size > 0) $.post('/filex/store-filesize?fs=' + file.size + '&fp=' + $encode(res.key))
+ if (file.size > 0) $.post('/filex/store-filesize?fs=' + file.size + '&fp=' + $encode(res.key) + useToken)
typeof complete === 'function' && complete({ key: res.key })
},
})
@@ -475,7 +477,7 @@ var $createUploader = function (input, next, complete, error) {
} else {
input.html5Uploader({
name: input.attr('id') || input.attr('name') || 'H5Upload',
- postUrl: rb.baseUrl + '/filex/upload?type=' + (imgOnly ? 'image' : 'file') + '&temp=' + (local === 'temp'),
+ postUrl: rb.baseUrl + '/filex/upload?type=' + (imgOnly ? 'image' : 'file') + '&temp=' + (local === 'temp') + useToken,
onSelectError: function (file, err) {
if (err === 'ErrorType') {
RbHighbar.create($L(imgOnly ? 'PlsUploadImg' : 'FileTypeError'))
@@ -492,7 +494,7 @@ var $createUploader = function (input, next, complete, error) {
onSuccess: function (e, file) {
e = $.parseJSON(e.currentTarget.response)
if (e.error_code === 0) {
- if (local !== 'temp' && file.size > 0) $.post('/filex/store-filesize?fs=' + file.size + '&fp=' + $encode(e.data))
+ if (local !== 'temp' && file.size > 0) $.post('/filex/store-filesize?fs=' + file.size + '&fp=' + $encode(e.data) + useToken)
complete({ key: e.data })
} else {
RbHighbar.error($L('ErrorUpload'))