Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b81cb91
fix(#790): escape file URL in bpmn.ftl with ?js_string to prevent Syn…
wylovelyi Sep 22, 2026
e34e281
fix(#790): escape file URL in csv.ftl with ?js_string to prevent Synt…
wylovelyi Sep 22, 2026
7343782
fix(#790): escape file URL in dcm.ftl with ?js_string to prevent Synt…
wylovelyi Sep 22, 2026
3096ef8
fix(#790): escape file URL in drawio.ftl with ?js_string to prevent S…
wylovelyi Sep 22, 2026
ab4542c
fix(#790): escape file URL in eml.ftl with ?js_string to prevent Synt…
wylovelyi Sep 22, 2026
94da320
fix(#790): escape file URL in epub.ftl with ?js_string to prevent Syn…
wylovelyi Sep 22, 2026
29ec825
fix(#790): escape file URL in msg.ftl with ?js_string to prevent Synt…
wylovelyi Sep 22, 2026
e8f1fff
fix(#790): escape file URL in ofd.ftl with ?js_string to prevent Synt…
wylovelyi Sep 22, 2026
f47e4b8
fix(#790): escape file URL in officeweb.ftl with ?js_string to preven…
wylovelyi Sep 22, 2026
228ac8e
fix(#790): escape file URL in online3D.ftl with ?js_string to prevent…
wylovelyi Sep 22, 2026
67b71de
fix(#790): escape file URL in svg.ftl with ?js_string to prevent Synt…
wylovelyi Sep 22, 2026
7a2250c
fix(#790): escape file URL in tiff.ftl with ?js_string to prevent Syn…
wylovelyi Sep 22, 2026
0094514
fix(#790): escape file URL in xmind.ftl with ?js_string to prevent Sy…
wylovelyi Sep 22, 2026
6fa1d91
fix(#790): escape file URL in pdf.ftl with ?js_string to prevent Synt…
wylovelyi Sep 22, 2026
016a040
test(#790): add regression test asserting file URL is JS-escaped in p…
wylovelyi Sep 22, 2026
ae7cdf3
test(#790): assert JS-escaping of file URL across all 14 preview temp…
wylovelyi Sep 24, 2026
7ef274d
fix: preserve spreadsheet URLs for JavaScript escaping
klboke Oct 8, 2026
1acd4e4
fix: exclude legacy schemas from Office preview dependencies
klboke Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,33 @@
<artifactId>poi</artifactId>
<groupId>org.apache.poi</groupId>
</exclusion>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>org.apache.poi.xwpf.converter.xhtml</artifactId>
<version>${xdocreport.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.xdocreport.document</artifactId>
<version>${xdocreport.version}</version>
<exclusions>
<exclusion>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.aspose</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,11 @@ public String filePreviewHandle(String url, Model model, FileAttribute fileAttri
if (!officePreviewType.equalsIgnoreCase("html")) {
if (ConfigConstants.getOfficeTypeWeb().equalsIgnoreCase("web")) {
if (suffix.equalsIgnoreCase("xlsx")) {
model.addAttribute("pdfUrl", KkFileUtils.htmlEscape(url)); //特殊符号处理
model.addAttribute("pdfUrl", url); //模板使用 ?js_string 按 JavaScript 字符串上下文转义
return XLSX_FILE_PREVIEW_PAGE;
}
if (suffix.equalsIgnoreCase("csv")) {
model.addAttribute("csvUrl", KkFileUtils.htmlEscape(url));
model.addAttribute("csvUrl", url);
return CSV_FILE_PREVIEW_PAGE;
}
}
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/bpmn.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
const viewer = new BpmnJS({
container: '#diagram'
});
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/csv.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var process_wb = (function() {
if(typeof console !== 'undefined') console.log("output", new Date());
};
})();
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/dcm.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
<script src="dcm/index.umd.js"></script>

<script>
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/drawio.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<#assign finalUrl="${baseUrl}${currentUrl}">
</#if>
<script>
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/eml.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<iframe src="" width="100%" frameborder="0"></iframe>
</body>
<script type="text/javascript">
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/epub.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<div id="next" class="arrow">›</div>
</body>
<script type="text/javascript">
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/msg.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<iframe src="" width="100%" frameborder="0"></iframe>
</body>
<script type="text/javascript">
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/ofd.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<iframe src="" width="100%" frameborder="0"></iframe>
</body>
<script type="text/javascript">
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';

Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/officeweb.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
window.location.href=test;
}

var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/online3D.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
<iframe src="" width="100%" frameborder="0"></iframe>
</body>
<script type="text/javascript">
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/pdf.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
</#if>

<script type="text/javascript">
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/svg.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
let isDragging = false;
let startX, startY, startTranslateX, startTranslateY;
let panStartX, panStartY;
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/tiff.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
var reg = new RegExp(str + "$");
return reg.test(this);
}
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/resources/web/xmind.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

<script src="xmind/xmind.js"></script>
<script type="text/javascript">
var url = '${finalUrl}';
var url = '${finalUrl?js_string}';
var kkagent = '${kkagent}';
var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
if (kkagent === 'true' || !url.startsWith(baseUrl)) {
Expand Down
19 changes: 19 additions & 0 deletions server/src/test/java/cn/keking/PdfViewerCompatibilityTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ void shouldPreferPdfForOfficePreviewByDefault() throws IOException {
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 {
try (InputStream inputStream = getClass().getResourceAsStream(resourcePath)) {
assertNotNull(inputStream);
Expand Down
76 changes: 76 additions & 0 deletions server/src/test/java/cn/keking/PreviewUrlEscapingTests.java
Original file line number Diff line number Diff line change
@@ -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("&#39;"), html);
} finally {
ConfigConstants.setOfficeTypeWebValue(previousOfficeTypeWeb);
}
}
}
Original file line number Diff line number Diff line change
@@ -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()));
}
}
Loading