Merge pull request #795 from wylovelyi/fix/issue-790-js-escape

fix: escape preview URLs and resolve Office schema conflicts
This commit is contained in:
kl
2026-10-08 13:19:56 +08:00
committed by GitHub
19 changed files with 191 additions and 16 deletions

View File

@@ -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>

View File

@@ -75,11 +75,11 @@ public class OfficeFilePreviewImpl implements FilePreview {
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;
}
}

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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}' + '/';

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -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)) {

View File

@@ -75,6 +75,25 @@ public class PdfViewerCompatibilityTests {
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);

View 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("&#39;"), html);
} finally {
ConfigConstants.setOfficeTypeWebValue(previousOfficeTypeWeb);
}
}
}

View File

@@ -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()));
}
}