mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-11-14 15:20:40 +00:00
Merge master into watermark defaults fix
This commit is contained in:
@@ -91,17 +91,33 @@
|
|||||||
<artifactId>poi</artifactId>
|
<artifactId>poi</artifactId>
|
||||||
<groupId>org.apache.poi</groupId>
|
<groupId>org.apache.poi</groupId>
|
||||||
</exclusion>
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.poi</groupId>
|
||||||
|
<artifactId>ooxml-schemas</artifactId>
|
||||||
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>fr.opensagres.xdocreport</groupId>
|
<groupId>fr.opensagres.xdocreport</groupId>
|
||||||
<artifactId>org.apache.poi.xwpf.converter.xhtml</artifactId>
|
<artifactId>org.apache.poi.xwpf.converter.xhtml</artifactId>
|
||||||
<version>${xdocreport.version}</version>
|
<version>${xdocreport.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.poi</groupId>
|
||||||
|
<artifactId>ooxml-schemas</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>fr.opensagres.xdocreport</groupId>
|
<groupId>fr.opensagres.xdocreport</groupId>
|
||||||
<artifactId>fr.opensagres.xdocreport.document</artifactId>
|
<artifactId>fr.opensagres.xdocreport.document</artifactId>
|
||||||
<version>${xdocreport.version}</version>
|
<version>${xdocreport.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.apache.poi</groupId>
|
||||||
|
<artifactId>ooxml-schemas</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aspose</groupId>
|
<groupId>com.aspose</groupId>
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
package cn.keking.service.cache.impl;
|
package cn.keking.service.cache.impl;
|
||||||
|
|
||||||
import cn.keking.service.cache.CacheService;
|
import cn.keking.service.cache.CacheService;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.redisson.api.RBlockingQueue;
|
import org.redisson.api.RBlockingQueue;
|
||||||
import org.redisson.api.RMapCache;
|
import org.redisson.api.RMapCache;
|
||||||
import org.redisson.api.RedissonClient;
|
import org.redisson.api.RedissonClient;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -68,6 +70,9 @@ public class CacheServiceRedisImpl implements CacheService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getImgCache(String key) {
|
public List<String> getImgCache(String key) {
|
||||||
|
if (StringUtils.isEmpty(key)) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
RMapCache<String, List<String>> convertedList = redissonClient.getMapCache(FILE_PREVIEW_IMGS_KEY);
|
RMapCache<String, List<String>> convertedList = redissonClient.getMapCache(FILE_PREVIEW_IMGS_KEY);
|
||||||
return convertedList.get(key);
|
return convertedList.get(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,11 +75,11 @@ public class OfficeFilePreviewImpl implements FilePreview {
|
|||||||
if (!officePreviewType.equalsIgnoreCase("html")) {
|
if (!officePreviewType.equalsIgnoreCase("html")) {
|
||||||
if (ConfigConstants.getOfficeTypeWeb().equalsIgnoreCase("web")) {
|
if (ConfigConstants.getOfficeTypeWeb().equalsIgnoreCase("web")) {
|
||||||
if (suffix.equalsIgnoreCase("xlsx")) {
|
if (suffix.equalsIgnoreCase("xlsx")) {
|
||||||
model.addAttribute("pdfUrl", KkFileUtils.htmlEscape(url)); //特殊符号处理
|
model.addAttribute("pdfUrl", url); //模板使用 ?js_string 按 JavaScript 字符串上下文转义
|
||||||
return XLSX_FILE_PREVIEW_PAGE;
|
return XLSX_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
if (suffix.equalsIgnoreCase("csv")) {
|
if (suffix.equalsIgnoreCase("csv")) {
|
||||||
model.addAttribute("csvUrl", KkFileUtils.htmlEscape(url));
|
model.addAttribute("csvUrl", url);
|
||||||
return CSV_FILE_PREVIEW_PAGE;
|
return CSV_FILE_PREVIEW_PAGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4053
server/src/main/resources/static/ofd/js/cnofd.umd.min.js
vendored
4053
server/src/main/resources/static/ofd/js/cnofd.umd.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -41,7 +41,7 @@
|
|||||||
const viewer = new BpmnJS({
|
const viewer = new BpmnJS({
|
||||||
container: '#diagram'
|
container: '#diagram'
|
||||||
});
|
});
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ var process_wb = (function() {
|
|||||||
if(typeof console !== 'undefined') console.log("output", new Date());
|
if(typeof console !== 'undefined') console.log("output", new Date());
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<script src="dcm/index.umd.js"></script>
|
<script src="dcm/index.umd.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<#assign finalUrl="${baseUrl}${currentUrl}">
|
<#assign finalUrl="${baseUrl}${currentUrl}">
|
||||||
</#if>
|
</#if>
|
||||||
<script>
|
<script>
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<iframe src="" width="100%" frameborder="0"></iframe>
|
<iframe src="" width="100%" frameborder="0"></iframe>
|
||||||
</body>
|
</body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div id="next" class="arrow">›</div>
|
<div id="next" class="arrow">›</div>
|
||||||
</body>
|
</body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<iframe src="" width="100%" frameborder="0"></iframe>
|
<iframe src="" width="100%" frameborder="0"></iframe>
|
||||||
</body>
|
</body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<iframe src="" width="100%" frameborder="0"></iframe>
|
<iframe src="" width="100%" frameborder="0"></iframe>
|
||||||
</body>
|
</body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
window.location.href=test;
|
window.location.href=test;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<iframe src="" width="100%" frameborder="0"></iframe>
|
<iframe src="" width="100%" frameborder="0"></iframe>
|
||||||
</body>
|
</body>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -58,6 +58,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 从原始图片 URL 提取文件名(优先用 data-original-url,避免受反代 / CORS 代理 URL 影响)
|
||||||
|
function getImageNameFromOriginalUrl(imageElement) {
|
||||||
|
var originalUrl = imageElement.getAttribute('data-original-url') || '';
|
||||||
|
// 去除查询串与锚点
|
||||||
|
var noQuery = originalUrl.split('?')[0].split('#')[0];
|
||||||
|
// 取路径最后一段作为文件名(兼容 / 与 \)
|
||||||
|
var lastSep = Math.max(noQuery.lastIndexOf('/'), noQuery.lastIndexOf('\\'));
|
||||||
|
var name = lastSep >= 0 ? noQuery.substring(lastSep + 1) : noQuery;
|
||||||
|
return name || 'image';
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化图片查看器
|
// 初始化图片查看器
|
||||||
function initImageViewer() {
|
function initImageViewer() {
|
||||||
var viewer = new Viewer(document.getElementById('image'), {
|
var viewer = new Viewer(document.getElementById('image'), {
|
||||||
@@ -66,6 +77,10 @@
|
|||||||
button: false,
|
button: false,
|
||||||
backdrop: false,
|
backdrop: false,
|
||||||
loop: true,
|
loop: true,
|
||||||
|
// Viewer 会将克隆图片的 data-original-url 改为加载地址,因此从原始元素读取标题。
|
||||||
|
title: function () {
|
||||||
|
return getImageNameFromOriginalUrl(this.images[this.index]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
viewer.view(0); // 0 是图片的索引,如果你想点击第一张图片,索引为 0
|
viewer.view(0); // 0 是图片的索引,如果你想点击第一张图片,索引为 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let startX, startY, startTranslateX, startTranslateY;
|
let startX, startY, startTranslateX, startTranslateY;
|
||||||
let panStartX, panStartY;
|
let panStartX, panStartY;
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
var reg = new RegExp(str + "$");
|
var reg = new RegExp(str + "$");
|
||||||
return reg.test(this);
|
return reg.test(this);
|
||||||
}
|
}
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<script src="xmind/xmind.js"></script>
|
<script src="xmind/xmind.js"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var url = '${finalUrl}';
|
var url = '${finalUrl?js_string}';
|
||||||
var kkagent = '${kkagent}';
|
var kkagent = '${kkagent}';
|
||||||
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
|
||||||
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
|
||||||
|
|||||||
@@ -75,6 +75,25 @@ public class PdfViewerCompatibilityTests {
|
|||||||
assertTrue(properties.contains("office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:pdf}"));
|
assertTrue(properties.contains("office.preview.type = ${KK_OFFICE_PREVIEW_TYPE:pdf}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldEscapeFileUrlWithJsStringInAllPreviewTemplates() throws IOException {
|
||||||
|
// Regression test for #790: a single quote in the source file URL (e.g. a path
|
||||||
|
// segment like "Int'l") was interpolated raw into the inline `var url = '...'`
|
||||||
|
// assignment, terminating the JS string early and throwing Uncaught SyntaxError.
|
||||||
|
// FreeMarker's ?js_string built-in escapes it for the JS string literal context.
|
||||||
|
// Every preview template that injects the file URL into a JS string literal must
|
||||||
|
// use ?js_string, otherwise a URL containing an apostrophe breaks the page.
|
||||||
|
String[] previewTemplates = {
|
||||||
|
"bpmn", "csv", "dcm", "drawio", "eml", "epub", "msg", "ofd",
|
||||||
|
"officeweb", "online3D", "pdf", "svg", "tiff", "xmind"
|
||||||
|
};
|
||||||
|
for (String name : previewTemplates) {
|
||||||
|
String template = readResource("/web/" + name + ".ftl");
|
||||||
|
assertTrue(template.contains("var url = '${finalUrl?js_string}';"),
|
||||||
|
() -> name + ".ftl must JS-escape the file URL to avoid SyntaxError on apostrophe (see #790)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String readResource(String resourcePath) throws IOException {
|
private String readResource(String resourcePath) throws IOException {
|
||||||
try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) {
|
try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) {
|
||||||
assertNotNull(inputStream);
|
assertNotNull(inputStream);
|
||||||
|
|||||||
52
server/src/test/java/cn/keking/PictureFilePreviewTests.java
Normal file
52
server/src/test/java/cn/keking/PictureFilePreviewTests.java
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package cn.keking;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* kkFileView #787 回归测试:直接图片预览(picture.ftl)必须在 Viewer.js 标题里显示文件名。
|
||||||
|
*
|
||||||
|
* 根因:v4.3.0 之后模板把图片元素从 <img> 改成 <div src>,而 HTMLDivElement 没有 DOM 的
|
||||||
|
* src 属性,Viewer.js 用 image.src 取标题得到 undefined,导致标题为空。修复方式是在 Viewer 初始化时
|
||||||
|
* 配置 title 函数,从 data-original-url 派生文件名。
|
||||||
|
*
|
||||||
|
* Viewer 会将内部克隆图片的 data-original-url 改为实际加载地址(可能是代理地址),
|
||||||
|
* 标题必须读取 this.images[this.index] 中保留原始 URL 的元素。
|
||||||
|
*/
|
||||||
|
public class PictureFilePreviewTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeriveViewerTitleFromOriginalUrl() throws IOException {
|
||||||
|
String pictureTemplate = readResource("/web/picture.ftl");
|
||||||
|
|
||||||
|
// 模板仍保留 data-original-url 供标题派生使用
|
||||||
|
assertTrue(pictureTemplate.contains("data-original-url"),
|
||||||
|
() -> "picture.ftl must keep data-original-url for title derivation (see #787)");
|
||||||
|
|
||||||
|
// Viewer 初始化必须配置 title 函数,否则 <div> 的 image.src 为 undefined、标题为空
|
||||||
|
assertTrue(pictureTemplate.contains("title: function"),
|
||||||
|
() -> "picture.ftl must configure a Viewer title function to show the filename (see #787)");
|
||||||
|
|
||||||
|
// 标题派生逻辑必须读取 data-original-url(而非反代 / CORS 代理 URL)
|
||||||
|
assertTrue(pictureTemplate.contains("getImageNameFromOriginalUrl"),
|
||||||
|
() -> "picture.ftl must derive the title from data-original-url (see #787)");
|
||||||
|
assertTrue(pictureTemplate.contains("getAttribute('data-original-url')"),
|
||||||
|
() -> "title derivation must read data-original-url attribute (see #787)");
|
||||||
|
|
||||||
|
assertTrue(pictureTemplate.contains("getImageNameFromOriginalUrl(this.images[this.index])"),
|
||||||
|
() -> "Viewer title must use the original element rather than its proxy-backed clone (see #787)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readResource(String resourcePath) throws IOException {
|
||||||
|
try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) {
|
||||||
|
assertNotNull(inputStream);
|
||||||
|
return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
server/src/test/java/cn/keking/PreviewUrlEscapingTests.java
Normal file
76
server/src/test/java/cn/keking/PreviewUrlEscapingTests.java
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package cn.keking;
|
||||||
|
|
||||||
|
import cn.keking.config.ConfigConstants;
|
||||||
|
import cn.keking.model.FileAttribute;
|
||||||
|
import cn.keking.service.impl.OfficeFilePreviewImpl;
|
||||||
|
import cn.keking.service.impl.PdfFilePreviewImpl;
|
||||||
|
import freemarker.template.Configuration;
|
||||||
|
import freemarker.template.Template;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.springframework.ui.ExtendedModelMap;
|
||||||
|
|
||||||
|
import java.io.StringWriter;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class PreviewUrlEscapingTests {
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = {"pdf", "csv", "xlsx"})
|
||||||
|
void shouldPreserveSourceUrlThroughPreviewModelAndTemplate(String suffix) throws Exception {
|
||||||
|
String sourceUrl = "https://files.example/Int'l/report%20one." + suffix + "?token=a&mode=b";
|
||||||
|
String previousOfficeTypeWeb = ConfigConstants.getOfficeTypeWeb();
|
||||||
|
ConfigConstants.setOfficeTypeWebValue("web");
|
||||||
|
try {
|
||||||
|
FileAttribute file = new FileAttribute();
|
||||||
|
file.setName("report." + suffix);
|
||||||
|
file.setSuffix(suffix);
|
||||||
|
file.setOfficePreviewType("pdf");
|
||||||
|
file.setCacheName("url-escaping-test." + suffix);
|
||||||
|
|
||||||
|
ExtendedModelMap model = new ExtendedModelMap();
|
||||||
|
model.addAttribute("file", file);
|
||||||
|
model.addAttribute("baseUrl", "https://preview.example/");
|
||||||
|
model.addAttribute("kkagent", "false");
|
||||||
|
model.addAttribute("page", "0");
|
||||||
|
model.addAttribute("switchDisabled", "true");
|
||||||
|
model.addAttribute("watermarkTxt", "");
|
||||||
|
model.addAttribute("xlsxshowtoolbar", false);
|
||||||
|
for (String name : new String[]{"watermarkXSpace", "watermarkYSpace", "watermarkAlpha",
|
||||||
|
"watermarkWidth", "watermarkHeight", "watermarkAngle"}) {
|
||||||
|
model.addAttribute(name, "0");
|
||||||
|
}
|
||||||
|
for (String name : new String[]{"watermarkFont", "watermarkFontsize", "watermarkColor",
|
||||||
|
"highlightall", "pdfPresentationModeDisable", "pdfOpenFileDisable", "pdfPrintDisable",
|
||||||
|
"pdfDownloadDisable", "pdfBookmarkDisable", "pdfDisableEditing", "pdfSidebarOpen", "kkkey"}) {
|
||||||
|
model.addAttribute(name, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
OfficeFilePreviewImpl office = new OfficeFilePreviewImpl(null, null, null, null);
|
||||||
|
String view = suffix.equals("pdf")
|
||||||
|
? new PdfFilePreviewImpl(null, null, office, null).filePreviewHandle(sourceUrl, model, file)
|
||||||
|
: office.filePreviewHandle(sourceUrl, model, file);
|
||||||
|
assertEquals(Map.of("pdf", "pdf", "csv", "csv", "xlsx", "officeweb").get(suffix), view);
|
||||||
|
assertEquals(sourceUrl, model.get(suffix.equals("csv") ? "csvUrl" : "pdfUrl"));
|
||||||
|
|
||||||
|
Configuration configuration = new Configuration(Configuration.VERSION_2_3_32);
|
||||||
|
configuration.setClassForTemplateLoading(getClass(), "/web");
|
||||||
|
configuration.setDefaultEncoding("UTF-8");
|
||||||
|
configuration.setClassicCompatible(true);
|
||||||
|
Template template = configuration.getTemplate(view + ".ftl");
|
||||||
|
StringWriter output = new StringWriter();
|
||||||
|
template.process(model, output);
|
||||||
|
String html = output.toString();
|
||||||
|
|
||||||
|
assertTrue(html.contains("var url = 'https://files.example/Int\\'l/report%20one."
|
||||||
|
+ suffix + "?token=a&mode=b';"), html);
|
||||||
|
assertFalse(html.contains("'"), html);
|
||||||
|
} finally {
|
||||||
|
ConfigConstants.setOfficeTypeWebValue(previousOfficeTypeWeb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
server/src/test/java/cn/keking/service/cache/impl/CacheServiceRedisImplTests.java
vendored
Normal file
65
server/src/test/java/cn/keking/service/cache/impl/CacheServiceRedisImplTests.java
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package cn.keking.service.cache.impl;
|
||||||
|
|
||||||
|
import cn.keking.service.cache.CacheService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.redisson.api.RMapCache;
|
||||||
|
import org.redisson.api.RedissonClient;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CacheServiceRedisImpl 回归测试。
|
||||||
|
*
|
||||||
|
* 背景:cache.type=redis 时预览单张图片,compressFileKey 为 null,
|
||||||
|
* 修复前会把 null 直接传给 Redisson,抛出 NullPointerException: map key can't be null。
|
||||||
|
*/
|
||||||
|
class CacheServiceRedisImplTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnEmptyListWhenKeyIsNull() {
|
||||||
|
RedissonClient redissonClient = mock(RedissonClient.class);
|
||||||
|
CacheServiceRedisImpl cacheService = new CacheServiceRedisImpl(redissonClient);
|
||||||
|
|
||||||
|
List<String> result = cacheService.getImgCache(null);
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
// 判空后不应再访问 Redis —— 修复前这里会调用 getMapCache 并抛出 NPE
|
||||||
|
verifyNoInteractions(redissonClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnEmptyListWhenKeyIsEmpty() {
|
||||||
|
RedissonClient redissonClient = mock(RedissonClient.class);
|
||||||
|
CacheServiceRedisImpl cacheService = new CacheServiceRedisImpl(redissonClient);
|
||||||
|
|
||||||
|
List<String> result = cacheService.getImgCache("");
|
||||||
|
|
||||||
|
assertNotNull(result);
|
||||||
|
assertTrue(result.isEmpty());
|
||||||
|
verifyNoInteractions(redissonClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReadFromRedisWhenKeyIsPresent() {
|
||||||
|
RedissonClient redissonClient = mock(RedissonClient.class);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
RMapCache<String, List<String>> mapCache = mock(RMapCache.class);
|
||||||
|
when(redissonClient.<String, List<String>>getMapCache(CacheService.FILE_PREVIEW_IMGS_KEY))
|
||||||
|
.thenReturn(mapCache);
|
||||||
|
when(mapCache.get("zip-key")).thenReturn(List.of("a.png", "b.png"));
|
||||||
|
|
||||||
|
CacheServiceRedisImpl cacheService = new CacheServiceRedisImpl(redissonClient);
|
||||||
|
|
||||||
|
List<String> result = cacheService.getImgCache("zip-key");
|
||||||
|
|
||||||
|
assertEquals(List.of("a.png", "b.png"), result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package cn.keking.utils;
|
||||||
|
|
||||||
|
import org.apache.poi.extractor.ExtractorFactory;
|
||||||
|
import org.apache.poi.extractor.POITextExtractor;
|
||||||
|
import org.apache.poi.xslf.usermodel.XMLSlideShow;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class OfficeSchemaCompatibilityTests {
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
Path directory;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldLoadOneSchemaDefinitionForEachOfficeFormat() throws Exception {
|
||||||
|
for (String schema : new String[]{
|
||||||
|
"org/openxmlformats/schemas/wordprocessingml/x2006/main/DocumentDocument.class",
|
||||||
|
"org/openxmlformats/schemas/presentationml/x2006/main/SldMasterDocument.class"}) {
|
||||||
|
List<URL> definitions = Collections.list(getClass().getClassLoader().getResources(schema));
|
||||||
|
assertEquals(1, definitions.size(), () -> "Conflicting Office schema definitions: " + definitions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReadDocxWithCurrentPoiSchemas() throws Exception {
|
||||||
|
Path file = directory.resolve("sample.docx");
|
||||||
|
try (XWPFDocument document = new XWPFDocument(); OutputStream output = Files.newOutputStream(file)) {
|
||||||
|
document.createParagraph().createRun().setText("Schema compatibility");
|
||||||
|
document.write(output);
|
||||||
|
}
|
||||||
|
assertReadable(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReadPptxWithCurrentPoiSchemas() throws Exception {
|
||||||
|
Path file = directory.resolve("sample.pptx");
|
||||||
|
try (XMLSlideShow document = new XMLSlideShow(); OutputStream output = Files.newOutputStream(file)) {
|
||||||
|
document.createSlide().createTextBox().setText("Schema compatibility");
|
||||||
|
document.write(output);
|
||||||
|
}
|
||||||
|
assertReadable(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertReadable(Path file) throws Exception {
|
||||||
|
try (InputStream input = Files.newInputStream(file);
|
||||||
|
POITextExtractor extractor = ExtractorFactory.createExtractor(input)) {
|
||||||
|
assertTrue(extractor.getText().contains("Schema compatibility"));
|
||||||
|
}
|
||||||
|
assertFalse(OfficeUtils.isPwdProtected(file.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
14
tests/e2e/specs/picture-title.spec.ts
Normal file
14
tests/e2e/specs/picture-title.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
const fixtureBase = process.env.FIXTURE_BASE_URL || 'http://127.0.0.1:18080';
|
||||||
|
|
||||||
|
for (const proxy of [false, true]) {
|
||||||
|
test(`image title retains the source filename with kkagent=${proxy}`, async ({ page }) => {
|
||||||
|
const source = `${fixtureBase}/sample.png`;
|
||||||
|
const encoded = encodeURIComponent(Buffer.from(source).toString('base64'));
|
||||||
|
const response = await page.goto(`/onlinePreview?url=${encoded}&kkagent=${proxy}`);
|
||||||
|
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
await expect(page.locator('.viewer-title')).toHaveText('sample.png');
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user