mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-09-13 08:24:55 +00:00
Compare commits
8 Commits
pdf-jbig2-
...
codex/fix-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a199b4a7fe | ||
|
|
c4288f7e59 | ||
|
|
e2bcb67d43 | ||
|
|
69b234fda2 | ||
|
|
56eceebef8 | ||
|
|
f852bf844b | ||
|
|
583208898f | ||
|
|
9c3fd82076 |
@@ -1,4 +1,4 @@
|
|||||||
FROM keking/kkfileview-base:5.0.0
|
FROM keking/kkfileview-base:5.0.0
|
||||||
ADD server/target/kkFileView-*.tar.gz /opt/
|
ADD server/target/kkFileView-*.tar.gz /opt/
|
||||||
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.1/bin
|
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.2/bin
|
||||||
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.1/config/application.properties","-jar","/opt/kkFileView-5.0.1/bin/kkFileView-5.0.1.jar"]
|
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.2/config/application.properties","-jar","/opt/kkFileView-5.0.2/bin/kkFileView-5.0.2.jar"]
|
||||||
|
|||||||
15
README.cn.md
15
README.cn.md
@@ -149,6 +149,21 @@ pdf预览模式预览效果如下
|
|||||||
|
|
||||||
### 历史更新记录
|
### 历史更新记录
|
||||||
|
|
||||||
|
#### > 2026年08月14日,v5.0.2 补丁版本发布 :
|
||||||
|
|
||||||
|
#### 安全修复
|
||||||
|
1. 将不可信 HTML 预览放入不具有同源权限的 iframe 沙箱,并默认禁用其中的 JavaScript,避免被预览文件在 kkFileView 应用源中执行脚本(GHSA-9wcf-jxxf-w2g2)
|
||||||
|
2. 默认禁用演示文件删除接口,将接口改为 POST,并要求显式配置密码后进行精确比较(GHSA-f3qx-xrwc-5428)
|
||||||
|
|
||||||
|
#### 修复问题
|
||||||
|
1. 在 PDF 转图服务启动时刷新 ImageIO 插件,使 JBIG2 等嵌套 JAR 图像读取器能够被发现,避免 PDF 转图片预览时部分图像丢失
|
||||||
|
|
||||||
|
#### 升级说明
|
||||||
|
1. 建议所有 v5.0.1 及更早版本用户尽快升级到 v5.0.2
|
||||||
|
2. 本版本继续要求 JDK 21 及以上,现有 v5.0.1 配置可直接沿用
|
||||||
|
3. 文件删除功能现在默认禁用;如确需启用,请通过 `KK_DELETE_PASSWORD` 或外部 `delete.password` 设置独立强密码,并将 `/deleteFile` 调用改为 POST
|
||||||
|
4. `kk.scriptjs` 现在默认为 `false`;显式启用后,脚本仍只会在隔离的 iframe 沙箱内运行
|
||||||
|
|
||||||
#### > 2026年07月13日,v5.0.1 补丁版本发布 :
|
#### > 2026年07月13日,v5.0.1 补丁版本发布 :
|
||||||
|
|
||||||
#### 安全修复
|
#### 安全修复
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -65,6 +65,21 @@ URL:[https://file.kkview.cn](https://file.kkview.cn)
|
|||||||
|
|
||||||
## Change History
|
## Change History
|
||||||
|
|
||||||
|
### Version 5.0.2 (August 14, 2026)
|
||||||
|
|
||||||
|
#### Security Fixes
|
||||||
|
1. Sandboxed untrusted HTML previews in an opaque-origin iframe and disabled embedded JavaScript by default, preventing previewed files from executing in the kkFileView application origin (GHSA-9wcf-jxxf-w2g2)
|
||||||
|
2. Disabled the demo file deletion endpoint by default, changed it to POST, and required an explicitly configured password with exact comparison (GHSA-f3qx-xrwc-5428)
|
||||||
|
|
||||||
|
#### Fixes
|
||||||
|
1. Refreshed ImageIO plugins when PDF conversion starts so nested JAR providers such as the JBIG2 reader are discovered, preventing images from disappearing in PDF-to-image previews
|
||||||
|
|
||||||
|
#### Upgrade Notes
|
||||||
|
1. All users running v5.0.1 or earlier are strongly encouraged to upgrade to v5.0.2
|
||||||
|
2. JDK 21 or higher remains required, and existing v5.0.1 configuration can be reused
|
||||||
|
3. File deletion is now disabled unless `KK_DELETE_PASSWORD` or an external `delete.password` is set to an independent strong password; integrations must call `/deleteFile` with POST
|
||||||
|
4. `kk.scriptjs` now defaults to `false`; when explicitly enabled, scripts still run only inside the isolated iframe sandbox
|
||||||
|
|
||||||
### Version 5.0.1 (July 13, 2026)
|
### Version 5.0.1 (July 13, 2026)
|
||||||
|
|
||||||
#### Security Fixes
|
#### Security Fixes
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>cn.keking</groupId>
|
<groupId>cn.keking</groupId>
|
||||||
<artifactId>kkFileView-parent</artifactId>
|
<artifactId>kkFileView-parent</artifactId>
|
||||||
<version>5.0.1</version>
|
<version>5.0.2</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- ========== Java 和编译配置 ========== -->
|
<!-- ========== Java 和编译配置 ========== -->
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>kkFileView-parent</artifactId>
|
<artifactId>kkFileView-parent</artifactId>
|
||||||
<groupId>cn.keking</groupId>
|
<groupId>cn.keking</groupId>
|
||||||
<version>5.0.1</version>
|
<version>5.0.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>kkFileView</artifactId>
|
<artifactId>kkFileView</artifactId>
|
||||||
|
|||||||
@@ -408,8 +408,9 @@ home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
|
|||||||
# 启用后删除文件需要输入验证码,防止误删
|
# 启用后删除文件需要输入验证码,防止误删
|
||||||
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
||||||
|
|
||||||
# 删除文件密码,默认为123456
|
# 删除文件密码,默认为false(禁用删除接口)
|
||||||
delete.password = ${KK_DELETE_PASSWORD:123456}
|
# 如需启用删除功能,请通过环境变量或外部配置设置独立的强密码
|
||||||
|
delete.password = ${KK_DELETE_PASSWORD:false}
|
||||||
|
|
||||||
# 是否删除转换后的源文件,默认为true(删除)
|
# 是否删除转换后的源文件,默认为true(删除)
|
||||||
# 启用可节约磁盘空间,但会丢失原始文件
|
# 启用可节约磁盘空间,但会丢失原始文件
|
||||||
@@ -469,8 +470,8 @@ kk.xlsxshowtoolbar = false
|
|||||||
# 首页是否显示key密钥 默认为false(禁用)
|
# 首页是否显示key密钥 默认为false(禁用)
|
||||||
kk.isshowkey = false
|
kk.isshowkey = false
|
||||||
|
|
||||||
# 预览html文件 是否启用JavaScript 默认为true(启用)
|
# 预览html文件 是否在隔离沙箱中启用JavaScript,默认为false(禁用)
|
||||||
kk.scriptjs = true
|
kk.scriptjs = false
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|||||||
@@ -405,8 +405,8 @@ home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
|
|||||||
# 启用后删除文件需要输入验证码,防止误删
|
# 启用后删除文件需要输入验证码,防止误删
|
||||||
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
||||||
|
|
||||||
# 删除文件密码,默认为123456
|
# 删除文件密码,默认为false(禁用删除接口)
|
||||||
delete.password = ${KK_DELETE_PASSWORD:123456}
|
delete.password = ${KK_DELETE_PASSWORD:false}
|
||||||
|
|
||||||
# 是否删除转换后的源文件,默认为true(删除)
|
# 是否删除转换后的源文件,默认为true(删除)
|
||||||
# 启用可节约磁盘空间,但会丢失原始文件
|
# 启用可节约磁盘空间,但会丢失原始文件
|
||||||
@@ -466,8 +466,8 @@ kk.xlsxshowtoolbar = true
|
|||||||
# 首页是否显示key密钥 默认为false(禁用)
|
# 首页是否显示key密钥 默认为false(禁用)
|
||||||
kk.isshowkey = true
|
kk.isshowkey = true
|
||||||
|
|
||||||
# 预览html文件 是否启用JavaScript 默认为true(启用)
|
# 预览html文件 是否在隔离沙箱中启用JavaScript,默认为false(禁用)
|
||||||
kk.scriptjs = true
|
kk.scriptjs = false
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class ConfigConstants {
|
|||||||
// ==================================================
|
// ==================================================
|
||||||
public static final String DEFAULT_VALUE = "default";
|
public static final String DEFAULT_VALUE = "default";
|
||||||
public static final String DEFAULT_SHOW_AES_KEY = "1234567890123456";
|
public static final String DEFAULT_SHOW_AES_KEY = "1234567890123456";
|
||||||
public static final String DEFAULT_PASSWORD = "123456";
|
public static final String DEFAULT_PASSWORD = "false";
|
||||||
public static final String DEFAULT_SIZE = "500MB";
|
public static final String DEFAULT_SIZE = "500MB";
|
||||||
public static final String DEFAULT_ENABLE_REFRECSHSCHEDULE = "5";
|
public static final String DEFAULT_ENABLE_REFRECSHSCHEDULE = "5";
|
||||||
public static final String DEFAULT_IS_JAVASCRIPT = "false";
|
public static final String DEFAULT_IS_JAVASCRIPT = "false";
|
||||||
@@ -664,7 +664,7 @@ public class ConfigConstants {
|
|||||||
public void setSize(String size) { setSizeValue(size); }
|
public void setSize(String size) { setSizeValue(size); }
|
||||||
public static void setSizeValue(String size) { ConfigConstants.size = size; }
|
public static void setSizeValue(String size) { ConfigConstants.size = size; }
|
||||||
|
|
||||||
@Value("${delete.password:123456}")
|
@Value("${delete.password:false}")
|
||||||
public void setPassword(String password) { setPasswordValue(password); }
|
public void setPassword(String password) { setPasswordValue(password); }
|
||||||
public static void setPasswordValue(String password) { ConfigConstants.password = password; }
|
public static void setPasswordValue(String password) { ConfigConstants.password = password; }
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import cn.keking.utils.WebUtils;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.util.StreamUtils;
|
import org.springframework.util.StreamUtils;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -33,6 +34,8 @@ import java.nio.file.InvalidPathException;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import static cn.keking.utils.CaptchaUtil.CAPTCHA_CODE;
|
import static cn.keking.utils.CaptchaUtil.CAPTCHA_CODE;
|
||||||
@@ -218,7 +221,7 @@ public class FileController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/deleteFile")
|
@PostMapping("/deleteFile")
|
||||||
public ReturnResponse<Object> deleteFile(HttpServletRequest request, String fileName, String password) {
|
public ReturnResponse<Object> deleteFile(HttpServletRequest request, String fileName, String password) {
|
||||||
ReturnResponse<Object> checkResult = this.deleteFileCheck(request, fileName, password);
|
ReturnResponse<Object> checkResult = this.deleteFileCheck(request, fileName, password);
|
||||||
if (checkResult.isFailure()) {
|
if (checkResult.isFailure()) {
|
||||||
@@ -775,11 +778,22 @@ public class FileController {
|
|||||||
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
|
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
|
||||||
}
|
}
|
||||||
|
|
||||||
String expectedPassword = ConfigConstants.getDeleteCaptcha() ?
|
boolean captchaEnabled = ConfigConstants.getDeleteCaptcha();
|
||||||
|
String expectedPassword = captchaEnabled ?
|
||||||
WebUtils.getSessionAttr(request, CAPTCHA_CODE) :
|
WebUtils.getSessionAttr(request, CAPTCHA_CODE) :
|
||||||
ConfigConstants.getPassword();
|
ConfigConstants.getPassword();
|
||||||
|
|
||||||
if (!password.equalsIgnoreCase(expectedPassword)) {
|
if (!captchaEnabled && (!StringUtils.hasText(expectedPassword)
|
||||||
|
|| "false".equalsIgnoreCase(expectedPassword))) {
|
||||||
|
return ReturnResponse.failure("文件删除接口已禁用,请先配置 delete.password");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StringUtils.hasText(expectedPassword)) {
|
||||||
|
return ReturnResponse.failure("验证码已失效,请刷新后重试!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MessageDigest.isEqual(password.getBytes(StandardCharsets.UTF_8),
|
||||||
|
expectedPassword.getBytes(StandardCharsets.UTF_8))) {
|
||||||
logger.error("删除文件【{}】失败,密码错误!", fileName);
|
logger.error("删除文件【{}】失败,密码错误!", fileName);
|
||||||
return ReturnResponse.failure("删除文件失败,密码错误!");
|
return ReturnResponse.failure("删除文件失败,密码错误!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
var kkhighlightAll;
|
var kkhighlightAll;
|
||||||
var watermarkTxt;
|
var watermarkTxt;
|
||||||
|
var watermarkSettings = {
|
||||||
|
start_x: 80,
|
||||||
|
start_y: 80,
|
||||||
|
x_space: 80,
|
||||||
|
y_space: 80,
|
||||||
|
color: 'black',
|
||||||
|
alpha: 0.2,
|
||||||
|
fontsize: '18px',
|
||||||
|
font: '微软雅黑',
|
||||||
|
width: 200,
|
||||||
|
height: 80,
|
||||||
|
angle: 30
|
||||||
|
};
|
||||||
const queryString = document.location.search.substring(1);
|
const queryString = document.location.search.substring(1);
|
||||||
const params = (0, parseQueryString)(queryString);
|
const params = (0, parseQueryString)(queryString);
|
||||||
|
|
||||||
@@ -13,6 +26,30 @@ if (kkpdfAutoFetch == "true") {
|
|||||||
function isNotEmpty(value) {
|
function isNotEmpty(value) {
|
||||||
return value !== null && value !== undefined && value !== '' && value !== 'false' ;
|
return value !== null && value !== undefined && value !== '' && value !== 'false' ;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getWatermarkStringParam(params, name, fallback) {
|
||||||
|
const value = params.get(name);
|
||||||
|
return isNotEmpty(value) ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWatermarkNumberParam(params, name, fallback, isValid) {
|
||||||
|
const rawValue = params.get(name);
|
||||||
|
if (!isNotEmpty(rawValue)) return fallback;
|
||||||
|
const value = Number(rawValue);
|
||||||
|
return Number.isFinite(value) && isValid(value) ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function configureWatermark(params) {
|
||||||
|
watermarkSettings.x_space = getWatermarkNumberParam(params, "watermarkxspace", watermarkSettings.x_space, value => value >= 0);
|
||||||
|
watermarkSettings.y_space = getWatermarkNumberParam(params, "watermarkyspace", watermarkSettings.y_space, value => value >= 0);
|
||||||
|
watermarkSettings.font = getWatermarkStringParam(params, "watermarkfont", watermarkSettings.font);
|
||||||
|
watermarkSettings.fontsize = getWatermarkStringParam(params, "watermarkfontsize", watermarkSettings.fontsize);
|
||||||
|
watermarkSettings.color = getWatermarkStringParam(params, "watermarkcolor", watermarkSettings.color);
|
||||||
|
watermarkSettings.alpha = getWatermarkNumberParam(params, "watermarkalpha", watermarkSettings.alpha, value => value >= 0 && value <= 1);
|
||||||
|
watermarkSettings.width = getWatermarkNumberParam(params, "watermarkwidth", watermarkSettings.width, value => value > 0);
|
||||||
|
watermarkSettings.height = getWatermarkNumberParam(params, "watermarkheight", watermarkSettings.height, value => value > 0);
|
||||||
|
watermarkSettings.angle = getWatermarkNumberParam(params, "watermarkangle", watermarkSettings.angle, Number.isFinite);
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 通用水印生成函数
|
* 通用水印生成函数
|
||||||
* @param {HTMLElement} container - 水印容器(相对定位的父元素)
|
* @param {HTMLElement} container - 水印容器(相对定位的父元素)
|
||||||
@@ -23,20 +60,7 @@ function isNotEmpty(value) {
|
|||||||
function addWatermark(container, watermarkTxt, explicitWidth = null, explicitHeight = null) {
|
function addWatermark(container, watermarkTxt, explicitWidth = null, explicitHeight = null) {
|
||||||
if (!isNotEmpty(watermarkTxt)) return;
|
if (!isNotEmpty(watermarkTxt)) return;
|
||||||
|
|
||||||
// 公共配置
|
const settings = watermarkSettings;
|
||||||
const settings = {
|
|
||||||
start_x: 80,
|
|
||||||
start_y: 80,
|
|
||||||
x_space: 80,
|
|
||||||
y_space: 80,
|
|
||||||
color: 'black',
|
|
||||||
alpha: 0.2,
|
|
||||||
fontsize: '18px',
|
|
||||||
font: '微软雅黑',
|
|
||||||
width: 200,
|
|
||||||
height: 80,
|
|
||||||
angle: 30
|
|
||||||
};
|
|
||||||
|
|
||||||
// 确定实际使用的宽高
|
// 确定实际使用的宽高
|
||||||
let pageWidth, pageHeight;
|
let pageWidth, pageHeight;
|
||||||
@@ -55,30 +79,30 @@ function addWatermark(container, watermarkTxt, explicitWidth = null, explicitHei
|
|||||||
maxY = Math.max(maxY, 250);
|
maxY = Math.max(maxY, 250);
|
||||||
|
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
for (let x = settings.start_x; x < maxX; x += settings.x_space) {
|
const xStep = settings.width + settings.x_space;
|
||||||
for (let y = settings.start_y; y < maxY; y += settings.y_space) {
|
const yStep = settings.height + settings.y_space;
|
||||||
|
for (let x = settings.start_x; x < maxX; x += xStep) {
|
||||||
|
for (let y = settings.start_y; y < maxY; y += yStep) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'mask_div';
|
div.className = 'mask_div';
|
||||||
div.appendChild(document.createTextNode(watermarkTxt));
|
div.appendChild(document.createTextNode(watermarkTxt));
|
||||||
div.style.cssText = `
|
div.style.filter = `progid:DXImageTransform.Microsoft.Alpha(opacity=${settings.alpha * 100})`;
|
||||||
filter: progid:DXImageTransform.Microsoft.Alpha(opacity=${settings.alpha * 100});
|
div.style.transform = `rotate(-${settings.angle}deg)`;
|
||||||
transform: rotate(-${settings.angle}deg);
|
div.style.visibility = 'visible';
|
||||||
visibility: visible;
|
div.style.position = 'absolute';
|
||||||
position: absolute;
|
div.style.left = `${x}px`;
|
||||||
left: ${x}px;
|
div.style.top = `${y}px`;
|
||||||
top: ${y}px;
|
div.style.overflow = 'hidden';
|
||||||
overflow: hidden;
|
div.style.zIndex = '100';
|
||||||
z-index: 100;
|
div.style.pointerEvents = 'none';
|
||||||
pointer-events: none;
|
div.style.opacity = settings.alpha;
|
||||||
opacity: ${settings.alpha};
|
div.style.fontSize = settings.fontsize;
|
||||||
font-size: ${settings.fontsize};
|
div.style.fontFamily = settings.font;
|
||||||
font-family: ${settings.font};
|
div.style.color = settings.color;
|
||||||
color: ${settings.color};
|
div.style.textAlign = 'center';
|
||||||
text-align: center;
|
div.style.width = `${settings.width}px`;
|
||||||
width: ${settings.width}px;
|
div.style.height = `${settings.height}px`;
|
||||||
height: ${settings.height}px;
|
div.style.display = 'block';
|
||||||
display: block;
|
|
||||||
`;
|
|
||||||
fragment.appendChild(div);
|
fragment.appendChild(div);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22069,7 +22093,8 @@ const PDFViewerApplication = {
|
|||||||
disableBookmark = params.get("disablebookmark") ?? 'false';
|
disableBookmark = params.get("disablebookmark") ?? 'false';
|
||||||
disableEditing = params.get("disableediting") ?? 'false';
|
disableEditing = params.get("disableediting") ?? 'false';
|
||||||
kkhighlightAll = params.get("pdfhighlightall") ?? 'false';
|
kkhighlightAll = params.get("pdfhighlightall") ?? 'false';
|
||||||
watermarkTxt= params.get('watermarktxt') ?? 'false';
|
watermarkTxt = params.get('watermarktxt') ?? 'false';
|
||||||
|
configureWatermark(params);
|
||||||
try {
|
try {
|
||||||
file = new URL(file).href;
|
file = new URL(file).href;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -16,6 +16,22 @@
|
|||||||
<script src="js/jsformat.js" type="text/javascript"></script>
|
<script src="js/jsformat.js" type="text/javascript"></script>
|
||||||
</#if>
|
</#if>
|
||||||
<script src="js/base64.min.js" type="text/javascript"></script>
|
<script src="js/base64.min.js" type="text/javascript"></script>
|
||||||
|
<style>
|
||||||
|
#htmlPreviewFrame {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 65vh;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
#htmlSource {
|
||||||
|
min-height: 65vh;
|
||||||
|
overflow: auto;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<input hidden id="textData" value="${textData}"/>
|
<input hidden id="textData" value="${textData}"/>
|
||||||
@@ -25,7 +41,7 @@
|
|||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">
|
<h4 class="panel-title">
|
||||||
<strong><font color="red"><input class="GLOkBtn" type="button" value="运行html" onclick="loadXmlData();" /></font></strong>
|
<strong><font color="red"><input class="GLOkBtn" type="button" value="在沙箱中运行html" onclick="loadXmlData();" /></font></strong>
|
||||||
<a data-toggle="collapse" data-parent="#accordion" onclick="loadText();">
|
<a data-toggle="collapse" data-parent="#accordion" onclick="loadText();">
|
||||||
${file.name}
|
${file.name}
|
||||||
</a>
|
</a>
|
||||||
@@ -40,57 +56,42 @@
|
|||||||
// 将Freemarker的布尔值传递给JavaScript
|
// 将Freemarker的布尔值传递给JavaScript
|
||||||
var scriptjs = ${scriptjs?c}; // ?c 将布尔值转换为字符串true/false
|
var scriptjs = ${scriptjs?c}; // ?c 将布尔值转换为字符串true/false
|
||||||
|
|
||||||
|
function decodePreviewText() {
|
||||||
|
var escapedText = Base64.decode($("#textData").val());
|
||||||
|
var decoder = document.createElement("textarea");
|
||||||
|
decoder.innerHTML = escapedText;
|
||||||
|
return decoder.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function replacePreviewContent(element) {
|
||||||
|
var container = document.getElementById("text");
|
||||||
|
while (container.firstChild) {
|
||||||
|
container.removeChild(container.firstChild);
|
||||||
|
}
|
||||||
|
container.appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*加载普通文本
|
*加载普通文本
|
||||||
*/
|
*/
|
||||||
function loadText() {
|
function loadText() {
|
||||||
var base64data = $("#textData").val()
|
var source = document.createElement("pre");
|
||||||
var div = document.getElementById("text");
|
source.id = "htmlSource";
|
||||||
div.innerHTML = ""; //
|
source.textContent = decodePreviewText();
|
||||||
var textData = Base64.decode(base64data);
|
replacePreviewContent(source);
|
||||||
textData = htmlttt(textData,1);
|
|
||||||
var textPreData = "<xmp style='background-color: #FFFFFF;overflow-y: scroll;border:none'>" + textData + "</xmp>";
|
|
||||||
$("#text").append(textPreData);
|
|
||||||
}
|
|
||||||
|
|
||||||
function htmlttt (str,txt){
|
|
||||||
var s = "";
|
|
||||||
if(str.length == 0) return "";
|
|
||||||
s = str.replace(/&/gi,"&");
|
|
||||||
s = s.replace(/</gi,"<");
|
|
||||||
s = s.replace(/>/gi,">");
|
|
||||||
s = s.replace(/ /gi," ");
|
|
||||||
s = s.replace(/'/gi,"\'");
|
|
||||||
s = s.replace(/"/gi,"\"");
|
|
||||||
s = s.replace(/javascript/g,"javascript ");
|
|
||||||
if (txt === 2){
|
|
||||||
s = s.replace(/<script/gi, "<script ");
|
|
||||||
s = s.replace(/javascript/g,"javascript ");
|
|
||||||
s = s.replace(/<\/script/gi, "</script ");
|
|
||||||
s = s.replace(/<iframe/gi, "<iframe ");
|
|
||||||
s = s.replace(/<\/iframe/gi, "</iframe ");
|
|
||||||
s = s.replace(/confirm/gi, "c&onfirm");
|
|
||||||
s = s.replace(/alert/gi, "a&lert");
|
|
||||||
s = s.replace(/eval/gi, "e&val");
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*加载运行
|
*加载运行
|
||||||
*/
|
*/
|
||||||
function loadXmlData() {
|
function loadXmlData() {
|
||||||
var base64data = $("#textData").val();
|
var frame = document.createElement("iframe");
|
||||||
var textData = Base64.decode(base64data);
|
frame.id = "htmlPreviewFrame";
|
||||||
|
frame.title = "HTML sandbox preview";
|
||||||
// 直接使用JavaScript变量进行判断
|
frame.setAttribute("sandbox", scriptjs ? "allow-scripts" : "");
|
||||||
if (scriptjs) {
|
frame.setAttribute("referrerpolicy", "no-referrer");
|
||||||
textData = htmlttt(textData, 1);
|
frame.srcdoc = decodePreviewText();
|
||||||
} else {
|
replacePreviewContent(frame);
|
||||||
textData = htmlttt(textData, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#text').html(textData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -369,8 +369,8 @@
|
|||||||
$("#deleteCaptchaConfirmBtn").click(function() {
|
$("#deleteCaptchaConfirmBtn").click(function() {
|
||||||
var fileName = $("#deleteCaptchaFileName").val();
|
var fileName = $("#deleteCaptchaFileName").val();
|
||||||
var deleteCaptchaText = $("#deleteCaptchaText").val();
|
var deleteCaptchaText = $("#deleteCaptchaText").val();
|
||||||
$.get('${baseUrl}deleteFile?fileName=' + fileName +'&password=' + deleteCaptchaText, function(data){
|
$.post('${baseUrl}deleteFile', {fileName: fileName, password: deleteCaptchaText}, function(data){
|
||||||
if ("删除文件失败,密码错误!" === data.msg) {
|
if (!data.success) {
|
||||||
alert(data.msg);
|
alert(data.msg);
|
||||||
} else {
|
} else {
|
||||||
$('#table').bootstrapTable("refresh", {});
|
$('#table').bootstrapTable("refresh", {});
|
||||||
@@ -392,11 +392,16 @@
|
|||||||
function deleteFile(fileName, isFolder) {
|
function deleteFile(fileName, isFolder) {
|
||||||
var message = isFolder ? '你确定要删除这个文件夹吗?(包含所有子文件)' : '你确定要删除这个文件吗?';
|
var message = isFolder ? '你确定要删除这个文件夹吗?(包含所有子文件)' : '你确定要删除这个文件吗?';
|
||||||
if (window.confirm(message)) {
|
if (window.confirm(message)) {
|
||||||
password = prompt("请输入默认密码:123456");
|
var password = prompt("请输入文件删除密码");
|
||||||
|
if (password === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '${baseUrl}deleteFile?fileName=' + fileName +'&password='+password,
|
url: '${baseUrl}deleteFile',
|
||||||
|
type: 'POST',
|
||||||
|
data: {fileName: fileName, password: password},
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
if ("删除文件失败,密码错误!" === data.msg) {
|
if (!data.success) {
|
||||||
alert(data.msg);
|
alert(data.msg);
|
||||||
} else {
|
} else {
|
||||||
$("#table").bootstrapTable("refresh", {});
|
$("#table").bootstrapTable("refresh", {});
|
||||||
|
|||||||
@@ -41,10 +41,10 @@
|
|||||||
你可以先看最新版本的升级重点,再顺着时间轴继续了解历史版本细节。
|
你可以先看最新版本的升级重点,再顺着时间轴继续了解历史版本细节。
|
||||||
</p>
|
</p>
|
||||||
<div class="release-badge-row">
|
<div class="release-badge-row">
|
||||||
<span class="tag highlight">最新版本 v5.0.1</span>
|
<span class="tag highlight">最新版本 v5.0.2</span>
|
||||||
<span class="tag brand">发布日期 2026-07-13</span>
|
<span class="tag brand">发布日期 2026-08-14</span>
|
||||||
<span class="tag warn">JDK 21+ 强制要求</span>
|
<span class="tag warn">JDK 21+ 强制要求</span>
|
||||||
<span class="tag">安全补丁 / PDF、Redis、XLSX 修复</span>
|
<span class="tag">安全补丁 / HTML、文件删除、PDF 转图修复</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -52,11 +52,50 @@
|
|||||||
<section class="release-section">
|
<section class="release-section">
|
||||||
<div class="timeline-year">2026</div>
|
<div class="timeline-year">2026</div>
|
||||||
<div class="timeline-list">
|
<div class="timeline-list">
|
||||||
|
<article class="release-card">
|
||||||
|
<h3>v5.0.2</h3>
|
||||||
|
<div class="release-meta">
|
||||||
|
<span class="tag brand">2026-08-14</span>
|
||||||
|
<span class="tag highlight">最新稳定版本</span>
|
||||||
|
<span class="tag warn">建议尽快升级</span>
|
||||||
|
</div>
|
||||||
|
<div class="release-columns">
|
||||||
|
<div class="release-group">
|
||||||
|
<h4>安全修复</h4>
|
||||||
|
<ul class="release-list">
|
||||||
|
<li>HTML 文件改在不具有同源权限的 iframe 沙箱中预览,并默认禁用 JavaScript。</li>
|
||||||
|
<li>文件删除接口默认禁用,改用 POST,并要求显式配置密码后进行精确比较。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="release-group">
|
||||||
|
<h4>修复</h4>
|
||||||
|
<ul class="release-list">
|
||||||
|
<li>刷新 ImageIO 插件,修复 PDF 转图片预览时 JBIG2 等图像读取器未被发现导致的图片丢失。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="release-group">
|
||||||
|
<h4>配置调整</h4>
|
||||||
|
<ul class="release-list">
|
||||||
|
<li><code>delete.password</code> 默认改为 <code>false</code>。</li>
|
||||||
|
<li><code>kk.scriptjs</code> 默认改为 <code>false</code>,启用后仍保持沙箱隔离。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="release-group">
|
||||||
|
<h4>升级重点</h4>
|
||||||
|
<ul class="release-list">
|
||||||
|
<li>建议所有 v5.0.1 及更早版本用户尽快升级。</li>
|
||||||
|
<li>继续要求 JDK 21 及以上,现有配置可直接沿用。</li>
|
||||||
|
<li>如需删除功能,请配置独立强密码,并将调用方式改为 POST。</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
<article class="release-card">
|
<article class="release-card">
|
||||||
<h3>v5.0.1</h3>
|
<h3>v5.0.1</h3>
|
||||||
<div class="release-meta">
|
<div class="release-meta">
|
||||||
<span class="tag brand">2026-07-13</span>
|
<span class="tag brand">2026-07-13</span>
|
||||||
<span class="tag highlight">最新稳定版本</span>
|
<span class="tag">上一补丁版本</span>
|
||||||
<span class="tag warn">建议尽快升级</span>
|
<span class="tag warn">建议尽快升级</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="release-columns">
|
<div class="release-columns">
|
||||||
|
|||||||
@@ -53,7 +53,18 @@
|
|||||||
url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(Base64.encode(url)) + "&key=${kkkey}";
|
url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(Base64.encode(url)) + "&key=${kkkey}";
|
||||||
}
|
}
|
||||||
var viewerUrl = baseUrl + "pdfjs/web/viewer.html?file=" + encodeURIComponent(url);
|
var viewerUrl = baseUrl + "pdfjs/web/viewer.html?file=" + encodeURIComponent(url);
|
||||||
var watermarkEncoded = encodeURIComponent('${watermarkTxt?js_string}');
|
var watermarkParams = {
|
||||||
|
watermarktxt: '${watermarkTxt?js_string}',
|
||||||
|
watermarkxspace: '${watermarkXSpace?js_string}',
|
||||||
|
watermarkyspace: '${watermarkYSpace?js_string}',
|
||||||
|
watermarkfont: '${watermarkFont?js_string}',
|
||||||
|
watermarkfontsize: '${watermarkFontsize?js_string}',
|
||||||
|
watermarkcolor: '${watermarkColor?js_string}',
|
||||||
|
watermarkalpha: '${watermarkAlpha?js_string}',
|
||||||
|
watermarkwidth: '${watermarkWidth?js_string}',
|
||||||
|
watermarkheight: '${watermarkHeight?js_string}',
|
||||||
|
watermarkangle: '${watermarkAngle?js_string}'
|
||||||
|
};
|
||||||
var highlightEncoded = encodeURIComponent('${highlightall?js_string}');
|
var highlightEncoded = encodeURIComponent('${highlightall?js_string}');
|
||||||
viewerUrl += "&disablepresentationmode=${pdfPresentationModeDisable}";
|
viewerUrl += "&disablepresentationmode=${pdfPresentationModeDisable}";
|
||||||
viewerUrl += "&disableopenfile=${pdfOpenFileDisable}";
|
viewerUrl += "&disableopenfile=${pdfOpenFileDisable}";
|
||||||
@@ -61,7 +72,9 @@
|
|||||||
viewerUrl += "&disabledownload=${pdfDownloadDisable}";
|
viewerUrl += "&disabledownload=${pdfDownloadDisable}";
|
||||||
viewerUrl += "&disablebookmark=${pdfBookmarkDisable}";
|
viewerUrl += "&disablebookmark=${pdfBookmarkDisable}";
|
||||||
viewerUrl += "&disableediting=${pdfDisableEditing}";
|
viewerUrl += "&disableediting=${pdfDisableEditing}";
|
||||||
viewerUrl += "&watermarktxt=" + watermarkEncoded;
|
Object.keys(watermarkParams).forEach(function (name) {
|
||||||
|
viewerUrl += "&" + name + "=" + encodeURIComponent(watermarkParams[name]);
|
||||||
|
});
|
||||||
viewerUrl += "&pdfhighlightall=" + highlightEncoded;
|
viewerUrl += "&pdfhighlightall=" + highlightEncoded;
|
||||||
viewerUrl += "#page=${page}"; // ?c 确保数字不包含千位分隔符
|
viewerUrl += "#page=${page}"; // ?c 确保数字不包含千位分隔符
|
||||||
<#if "true" == pdfSidebarOpen>
|
<#if "true" == pdfSidebarOpen>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -35,6 +36,38 @@ public class PdfViewerCompatibilityTests {
|
|||||||
assertTrue(pdfTemplate.contains("viewerUrl += \"&pagemode=none\";"));
|
assertTrue(pdfTemplate.contains("viewerUrl += \"&pagemode=none\";"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldForwardAndApplyAllPdfWatermarkSettings() throws IOException {
|
||||||
|
String pdfTemplate = readResource("/web/pdf.ftl");
|
||||||
|
String viewerScript = readResource("/static/pdfjs/web/viewer.mjs");
|
||||||
|
Map<String, String> watermarkParams = Map.ofEntries(
|
||||||
|
Map.entry("watermarktxt", "watermarkTxt"),
|
||||||
|
Map.entry("watermarkxspace", "watermarkXSpace"),
|
||||||
|
Map.entry("watermarkyspace", "watermarkYSpace"),
|
||||||
|
Map.entry("watermarkfont", "watermarkFont"),
|
||||||
|
Map.entry("watermarkfontsize", "watermarkFontsize"),
|
||||||
|
Map.entry("watermarkcolor", "watermarkColor"),
|
||||||
|
Map.entry("watermarkalpha", "watermarkAlpha"),
|
||||||
|
Map.entry("watermarkwidth", "watermarkWidth"),
|
||||||
|
Map.entry("watermarkheight", "watermarkHeight"),
|
||||||
|
Map.entry("watermarkangle", "watermarkAngle")
|
||||||
|
);
|
||||||
|
|
||||||
|
watermarkParams.forEach((queryParam, templateAttribute) -> {
|
||||||
|
assertTrue(pdfTemplate.contains(queryParam + ": '${" + templateAttribute + "?js_string}'"),
|
||||||
|
() -> "PDF template does not forward " + templateAttribute);
|
||||||
|
assertTrue(viewerScript.contains("\"" + queryParam + "\"")
|
||||||
|
|| viewerScript.contains("'" + queryParam + "'"),
|
||||||
|
() -> "PDF viewer does not consume " + queryParam);
|
||||||
|
});
|
||||||
|
assertTrue(viewerScript.contains("div.style.fontFamily = settings.font;"));
|
||||||
|
assertTrue(viewerScript.contains("div.style.fontSize = settings.fontsize;"));
|
||||||
|
assertTrue(viewerScript.contains("div.style.color = settings.color;"));
|
||||||
|
assertTrue(viewerScript.contains("div.style.opacity = settings.alpha;"));
|
||||||
|
assertTrue(viewerScript.contains("const xStep = settings.width + settings.x_space;"));
|
||||||
|
assertTrue(viewerScript.contains("const yStep = settings.height + settings.y_space;"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldPreferPdfForOfficePreviewByDefault() throws IOException {
|
void shouldPreferPdfForOfficePreviewByDefault() throws IOException {
|
||||||
String properties = readResource("/application.properties");
|
String properties = readResource("/application.properties");
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package cn.keking.web;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class HtmlPreviewSandboxTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRenderHtmlOnlyInsideAnOpaqueOriginSandbox() throws IOException {
|
||||||
|
String template = readResource("web/code.ftl");
|
||||||
|
|
||||||
|
assertTrue(template.contains("frame.setAttribute(\"sandbox\", scriptjs ? \"allow-scripts\" : \"\")"));
|
||||||
|
assertTrue(template.contains("frame.srcdoc = decodePreviewText()"));
|
||||||
|
assertFalse(template.contains("allow-same-origin"));
|
||||||
|
assertFalse(template.contains("$('#text').html(textData)"));
|
||||||
|
assertFalse(template.contains("function htmlttt"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDisplaySourceAsTextAndDisableScriptsByDefault() throws IOException {
|
||||||
|
String template = readResource("web/code.ftl");
|
||||||
|
String properties = readResource("application.properties");
|
||||||
|
|
||||||
|
assertTrue(template.contains("source.textContent = decodePreviewText()"));
|
||||||
|
assertTrue(properties.contains("kk.scriptjs = false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readResource(String path) throws IOException {
|
||||||
|
ClassPathResource resource = new ClassPathResource(path);
|
||||||
|
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package cn.keking.web.controller;
|
||||||
|
|
||||||
|
import cn.keking.config.ConfigConstants;
|
||||||
|
import cn.keking.model.ReturnResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class FileControllerDeleteSecurityTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path tempDir;
|
||||||
|
|
||||||
|
private String originalFileDir;
|
||||||
|
private String originalPassword;
|
||||||
|
private Boolean originalDeleteCaptcha;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void configureDemoDirectory() throws IOException {
|
||||||
|
originalFileDir = ConfigConstants.getFileDir();
|
||||||
|
originalPassword = ConfigConstants.getPassword();
|
||||||
|
originalDeleteCaptcha = ConfigConstants.getDeleteCaptcha();
|
||||||
|
Files.createDirectory(tempDir.resolve("demo"));
|
||||||
|
ConfigConstants.setFileDirValue(tempDir.toString());
|
||||||
|
ConfigConstants.setDeleteCaptchaValue(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void restoreConfiguration() {
|
||||||
|
ConfigConstants.setFileDirValue(originalFileDir);
|
||||||
|
ConfigConstants.setPasswordValue(originalPassword);
|
||||||
|
ConfigConstants.setDeleteCaptchaValue(originalDeleteCaptcha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDisableDeletionWhenNoPasswordIsConfigured() throws IOException {
|
||||||
|
ConfigConstants.setPasswordValue("false");
|
||||||
|
Path victim = Files.writeString(tempDir.resolve("demo/victim.txt"), "keep");
|
||||||
|
FileController controller = new FileController();
|
||||||
|
|
||||||
|
ReturnResponse<Object> response = controller.deleteFile(
|
||||||
|
new MockHttpServletRequest(), encodeFileName("victim.txt"), "false");
|
||||||
|
|
||||||
|
assertTrue(response.isFailure());
|
||||||
|
assertTrue(Files.exists(victim));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldRequireAnExactCaseSensitivePassword() throws IOException {
|
||||||
|
ConfigConstants.setPasswordValue("Strong-Delete-Password");
|
||||||
|
Path victim = Files.writeString(tempDir.resolve("demo/victim.txt"), "delete me");
|
||||||
|
FileController controller = new FileController();
|
||||||
|
|
||||||
|
ReturnResponse<Object> wrongCase = controller.deleteFile(
|
||||||
|
new MockHttpServletRequest(), encodeFileName("victim.txt"), "strong-delete-password");
|
||||||
|
assertTrue(wrongCase.isFailure());
|
||||||
|
assertTrue(Files.exists(victim));
|
||||||
|
|
||||||
|
ReturnResponse<Object> correct = controller.deleteFile(
|
||||||
|
new MockHttpServletRequest(), encodeFileName("victim.txt"), "Strong-Delete-Password");
|
||||||
|
assertTrue(correct.isSuccess());
|
||||||
|
assertFalse(Files.exists(victim));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldExposeDeletionOnlyAsPost() throws NoSuchMethodException {
|
||||||
|
Method method = FileController.class.getMethod(
|
||||||
|
"deleteFile", HttpServletRequest.class, String.class, String.class);
|
||||||
|
|
||||||
|
assertNotNull(method.getAnnotation(PostMapping.class));
|
||||||
|
assertNull(method.getAnnotation(GetMapping.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldKeepDeletionDisabledAndCredentialsOutOfUrlsByDefault() throws IOException {
|
||||||
|
String properties = readResource("application.properties");
|
||||||
|
String template = readResource("web/main/index.ftl");
|
||||||
|
|
||||||
|
assertTrue(properties.contains("delete.password = ${KK_DELETE_PASSWORD:false}"));
|
||||||
|
assertTrue(template.contains("type: 'POST'"));
|
||||||
|
assertTrue(template.contains("$.post('${baseUrl}deleteFile'"));
|
||||||
|
assertFalse(template.contains("deleteFile?"));
|
||||||
|
assertFalse(template.contains("默认密码:123456"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String encodeFileName(String fileName) {
|
||||||
|
String value = "file://localhost/" + fileName;
|
||||||
|
return Base64.encodeBase64String(value.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readResource(String path) throws IOException {
|
||||||
|
ClassPathResource resource = new ClassPathResource(path);
|
||||||
|
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -172,3 +172,53 @@ test('21 security: block 10.x host in getCorsFile', async ({ request }) => {
|
|||||||
const body = await resp.text();
|
const body = await resp.text();
|
||||||
expect(body).toContain('不受信任');
|
expect(body).toContain('不受信任');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('22 pdf preview applies custom watermark settings', async ({ page }) => {
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
url: b64(`${fixtureBase}/sample.pdf`),
|
||||||
|
watermarkTxt: 'Custom watermark',
|
||||||
|
watermarkXSpace: '37',
|
||||||
|
watermarkYSpace: '43',
|
||||||
|
watermarkFont: 'Courier New',
|
||||||
|
watermarkFontsize: '31px',
|
||||||
|
watermarkColor: 'rgb(1, 2, 3)',
|
||||||
|
watermarkAlpha: '0.65',
|
||||||
|
watermarkWidth: '100',
|
||||||
|
watermarkHeight: '77',
|
||||||
|
watermarkAngle: '17',
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`/onlinePreview?${query.toString()}`);
|
||||||
|
const watermark = page.frameLocator('#pdfFrame').locator('.mask_div').first();
|
||||||
|
await expect(watermark).toHaveText('Custom watermark');
|
||||||
|
|
||||||
|
const style = await watermark.evaluate(element => ({
|
||||||
|
fontFamily: element.style.fontFamily,
|
||||||
|
fontSize: element.style.fontSize,
|
||||||
|
color: element.style.color,
|
||||||
|
opacity: element.style.opacity,
|
||||||
|
width: element.style.width,
|
||||||
|
height: element.style.height,
|
||||||
|
transform: element.style.transform,
|
||||||
|
}));
|
||||||
|
expect(style).toEqual({
|
||||||
|
fontFamily: '"Courier New"',
|
||||||
|
fontSize: '31px',
|
||||||
|
color: 'rgb(1, 2, 3)',
|
||||||
|
opacity: '0.65',
|
||||||
|
width: '100px',
|
||||||
|
height: '77px',
|
||||||
|
transform: 'rotate(-17deg)',
|
||||||
|
});
|
||||||
|
|
||||||
|
const positions = await page.frameLocator('#pdfFrame').locator('.mask_div').evaluateAll(elements =>
|
||||||
|
elements.map(element => ({
|
||||||
|
left: Number.parseFloat(element.style.left),
|
||||||
|
top: Number.parseFloat(element.style.top),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const nextColumn = positions.find(position => position.left !== positions[0].left);
|
||||||
|
expect(positions[1].top - positions[0].top).toBe(120);
|
||||||
|
expect(nextColumn).toBeDefined();
|
||||||
|
expect(nextColumn!.left - positions[0].left).toBe(137);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user