From c4e1929ecf948c538bc06288f458903b9108839e Mon Sep 17 00:00:00 2001 From: yuhaibo Date: Tue, 8 Sep 2026 00:54:15 -0700 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E6=B0=B4=E5=8D=B0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=BC=BA=E5=A4=B1=E6=97=B6=20xlsx=20=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E7=99=BD=E9=A1=B5=20=E2=80=94=E2=80=94=20officeweb.ftl=20?= =?UTF-8?q?=E8=A1=A5=E4=B8=8A=E7=BC=BA=E7=9C=81=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 未提供外部 config/application.properties 的部署上,预览任何 xlsx 都失败: FreeMarker 在 officeweb.ftl 第 25 行抛 InvalidReferenceException,点名 watermarkTxt。DEBUG 模式把错误内联进 HTML,页面断在半截 JavaScript 里、 永远没有 ,浏览器上是白页或一直转圈,而 HTTP 状态码是 200 —— 看起来像转换超时,不像模板错。 两处叠在一起: 1. 十个 watermark* 请求属性可能整族不存在。WatermarkConfigConstants 没有 类级注解(对比 ConfigConstants 上的 @Component),不是 Spring bean, 那十个 @Value 的 setter 从来不会被调用;唯一填充它们的 ConfigRefreshComponent#loadConfig() 在配置文件不存在时直接 return。 于是十个 getter 全返回 null,AttributeSetFilter 的 setAttribute(name, null) 按 Servlet 规范等同于删除属性。 2. officeweb.ftl 是唯一没有 classic_compatible 兜底的预览模板。 commonHeader.ftl 第 1 行是 <#setting classic_compatible=true>, 所有 include 它的模板在属性缺失时把变量渲染成空串、照常出页面; officeweb.ftl 既不 include 它、自己也没有这个设置。 缺省值取 WatermarkConfigConstants.DEFAULT_*,watermarkTxt 取空串—— 空串会让下一行的 if (watermarkTxt !== '') 跳过 watermark.init(),不会给 所有预览凭空加上水印。其余九个只在水印开着时才用到,但那个 if 是 JavaScript 的、不是模板的,FreeMarker 两条分支都渲染,所以十个都要给。 新增的测试直接用 FreeMarker 渲染这个模板,不需要把服务跑起来: 数据模型里一个 watermark* 都不给(那正是缺陷发生时的情形),断言渲染得出 ;另一条把十个值全给上,断言逐个原样渲染,保证缺省值不会盖掉真实配置。 改模板之前先跑过一遍,第一条报 InvalidReferenceException、第二条通过。 Co-Authored-By: Claude Opus 5 (1M context) --- server/src/main/resources/web/officeweb.ftl | 22 ++--- .../web/OfficeWebWatermarkDefaultsTests.java | 92 +++++++++++++++++++ 2 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 server/src/test/java/cn/keking/web/OfficeWebWatermarkDefaultsTests.java diff --git a/server/src/main/resources/web/officeweb.ftl b/server/src/main/resources/web/officeweb.ftl index b1ed22276..8f6c2b77e 100644 --- a/server/src/main/resources/web/officeweb.ftl +++ b/server/src/main/resources/web/officeweb.ftl @@ -22,23 +22,23 @@ * 初始化水印 */ function initWaterMark() { - let watermarkTxt = '${watermarkTxt}'; + let watermarkTxt = '${watermarkTxt!''}'; if (watermarkTxt !== '') { watermark.init({ - watermark_txt: '${watermarkTxt}', + watermark_txt: '${watermarkTxt!''}', watermark_x: 0, watermark_y: 0, watermark_rows: 0, watermark_cols: 0, - watermark_x_space: ${watermarkXSpace}, - watermark_y_space: ${watermarkYSpace}, - watermark_font: '${watermarkFont}', - watermark_fontsize: '${watermarkFontsize}', - watermark_color: '${watermarkColor}', - watermark_alpha: ${watermarkAlpha}, - watermark_width: ${watermarkWidth}, - watermark_height: ${watermarkHeight}, - watermark_angle: ${watermarkAngle}, + watermark_x_space: ${watermarkXSpace!10}, + watermark_y_space: ${watermarkYSpace!10}, + watermark_font: '${watermarkFont!'微软雅黑'}', + watermark_fontsize: '${watermarkFontsize!'18px'}', + watermark_color: '${watermarkColor!'black'}', + watermark_alpha: ${watermarkAlpha!0.2}, + watermark_width: ${watermarkWidth!240}, + watermark_height: ${watermarkHeight!80}, + watermark_angle: ${watermarkAngle!10}, }); } } diff --git a/server/src/test/java/cn/keking/web/OfficeWebWatermarkDefaultsTests.java b/server/src/test/java/cn/keking/web/OfficeWebWatermarkDefaultsTests.java new file mode 100644 index 000000000..b71b571b2 --- /dev/null +++ b/server/src/test/java/cn/keking/web/OfficeWebWatermarkDefaultsTests.java @@ -0,0 +1,92 @@ +package cn.keking.web; + +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateExceptionHandler; +import org.junit.jupiter.api.Test; + +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * officeweb.ftl is the only preview template that neither includes commonHeader.ftl nor + * turns on classic_compatible, so it is the only one that fails outright when the + * watermark request attributes are absent. + * + * They are absent whenever WatermarkConfigConstants has not been populated: it is not a + * Spring bean, so its @Value defaults never run, and ConfigRefreshComponent#loadConfig + * returns early when the external config/application.properties is missing. Every getter + * then returns null, and AttributeSetFilter's setAttribute(name, null) removes the + * attribute per the Servlet spec. + * + * The first test therefore renders with a model that carries no watermark* entry at all. + * Do not add them "to make it realistic" - that is the condition under test. + */ +class OfficeWebWatermarkDefaultsTests { + + @Test + void shouldRenderXlsxPreviewPageWhenWatermarkAttributesAreMissing() throws Exception { + String html = render(baseModel()); + + assertTrue(html.contains(""), "page was truncated before "); + assertTrue(html.contains("let watermarkTxt = '';"), "watermarkTxt did not fall back to an empty string"); + assertTrue(html.contains("watermark_width: 240,"), "watermarkWidth did not fall back to its declared default"); + assertTrue(html.contains("watermark_font: '\u5fae\u8f6f\u96c5\u9ed1',"), "watermarkFont did not fall back to its declared default"); + } + + @Test + void shouldKeepConfiguredWatermarkSettingsWhenAttributesArePresent() throws Exception { + Map model = baseModel(); + model.put("watermarkTxt", "kkFileView"); + model.put("watermarkXSpace", "20"); + model.put("watermarkYSpace", "30"); + model.put("watermarkFont", "Arial"); + model.put("watermarkFontsize", "24px"); + model.put("watermarkColor", "red"); + model.put("watermarkAlpha", "0.5"); + model.put("watermarkWidth", "300"); + model.put("watermarkHeight", "120"); + model.put("watermarkAngle", "45"); + + String html = render(model); + + assertTrue(html.contains("let watermarkTxt = 'kkFileView';")); + assertTrue(html.contains("watermark_x_space: 20,")); + assertTrue(html.contains("watermark_y_space: 30,")); + assertTrue(html.contains("watermark_font: 'Arial',")); + assertTrue(html.contains("watermark_fontsize: '24px',")); + assertTrue(html.contains("watermark_color: 'red',")); + assertTrue(html.contains("watermark_alpha: 0.5,")); + assertTrue(html.contains("watermark_width: 300,")); + assertTrue(html.contains("watermark_height: 120,")); + assertTrue(html.contains("watermark_angle: 45,")); + } + + /** Everything officeweb.ftl needs except the watermark attributes. */ + private Map baseModel() { + Map model = new HashMap<>(); + model.put("file", Map.of("name", "demo.xlsx")); + model.put("pdfUrl", "demo.xlsx"); + model.put("baseUrl", "http://127.0.0.1:8012/"); + model.put("kkagent", "false"); + model.put("kkkey", ""); + model.put("xlsxshowtoolbar", Boolean.TRUE); + model.put("xlsxallowEdit", Boolean.FALSE); + return model; + } + + private String render(Map model) throws Exception { + Configuration configuration = new Configuration(Configuration.VERSION_2_3_32); + configuration.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "web"); + configuration.setDefaultEncoding("UTF-8"); + configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + + Template template = configuration.getTemplate("officeweb.ftl"); + StringWriter out = new StringWriter(); + template.process(model, out); + return out.toString(); + } +} From 45d7e34678629eee35228a6d600e12122d48319a Mon Sep 17 00:00:00 2001 From: yuhaibo Date: Sun, 27 Sep 2026 08:48:33 -0700 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20officeweb.ftl=20=E6=95=B0=E5=80=BC?= =?UTF-8?q?=E6=B0=B4=E5=8D=B0=E7=BC=BA=E7=9C=81=E5=80=BC=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2=EF=BC=8C=E9=81=BF=E5=85=8D=E6=8C=89?= =?UTF-8?q?=20locale=20=E6=A0=BC=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版给六个数值水印变量用的是数值缺省值(${watermarkAlpha!0.2} 等)。 FreeMarker 按请求的 locale 格式化数值:Accept-Language 为 de-DE / fr-FR 时 0.2 输出成 0,2,ar-EG 时数字变成阿拉伯-印度数字,内联脚本解析失败, initWaterMark 与 isLoading 都没有定义,xlsx 预览照样起不来——而此时响应是 200、 页面也有 ,只看页面完整性看不出来。 改为字符串缺省值,与 WatermarkConfigConstants.DEFAULT_* 的类型一致,原样输出。 已配置的值本来就是字符串,不受影响。 测试改为按 zh-CN / en-US / de-DE / fr-FR / ar-EG 逐个渲染,并逐个断言全部数值 输出;已有配置那条也按 zh-CN / de-DE / ar-EG 跑。改模板前先跑:de-DE、fr-FR 红在 watermark_alpha: 0,2,ar-EG 红在 watermark_x_space: ١٠,其余五条通过; 改后 8 条全过。 Co-Authored-By: Claude Opus 5.5 (1M context) --- server/src/main/resources/web/officeweb.ftl | 12 ++-- .../web/OfficeWebWatermarkDefaultsTests.java | 66 +++++++++++++------ 2 files changed, 52 insertions(+), 26 deletions(-) diff --git a/server/src/main/resources/web/officeweb.ftl b/server/src/main/resources/web/officeweb.ftl index 8f6c2b77e..f174ad183 100644 --- a/server/src/main/resources/web/officeweb.ftl +++ b/server/src/main/resources/web/officeweb.ftl @@ -30,15 +30,15 @@ watermark_y: 0, watermark_rows: 0, watermark_cols: 0, - watermark_x_space: ${watermarkXSpace!10}, - watermark_y_space: ${watermarkYSpace!10}, + watermark_x_space: ${watermarkXSpace!'10'}, + watermark_y_space: ${watermarkYSpace!'10'}, watermark_font: '${watermarkFont!'微软雅黑'}', watermark_fontsize: '${watermarkFontsize!'18px'}', watermark_color: '${watermarkColor!'black'}', - watermark_alpha: ${watermarkAlpha!0.2}, - watermark_width: ${watermarkWidth!240}, - watermark_height: ${watermarkHeight!80}, - watermark_angle: ${watermarkAngle!10}, + watermark_alpha: ${watermarkAlpha!'0.2'}, + watermark_width: ${watermarkWidth!'240'}, + watermark_height: ${watermarkHeight!'80'}, + watermark_angle: ${watermarkAngle!'10'}, }); } } diff --git a/server/src/test/java/cn/keking/web/OfficeWebWatermarkDefaultsTests.java b/server/src/test/java/cn/keking/web/OfficeWebWatermarkDefaultsTests.java index b71b571b2..70c4cb130 100644 --- a/server/src/test/java/cn/keking/web/OfficeWebWatermarkDefaultsTests.java +++ b/server/src/test/java/cn/keking/web/OfficeWebWatermarkDefaultsTests.java @@ -3,10 +3,12 @@ import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateExceptionHandler; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.io.StringWriter; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -24,21 +26,36 @@ * * The first test therefore renders with a model that carries no watermark* entry at all. * Do not add them "to make it realistic" - that is the condition under test. + * + * The template's defaults are string literals on purpose: a numeric default goes through + * FreeMarker's locale-aware number format, so under de_DE 0.2 renders as "0,2" and under + * ar_EG the digits become Arabic-Indic - either way the inline script no longer parses. + * The locale comes from the request's Accept-Language, which is why both tests run per + * locale and every numeric value is asserted, not just one. */ class OfficeWebWatermarkDefaultsTests { - @Test - void shouldRenderXlsxPreviewPageWhenWatermarkAttributesAreMissing() throws Exception { - String html = render(baseModel()); + @ParameterizedTest + @ValueSource(strings = {"zh-CN", "en-US", "de-DE", "fr-FR", "ar-EG"}) + void shouldRenderXlsxPreviewPageWhenWatermarkAttributesAreMissing(String languageTag) throws Exception { + String html = render(baseModel(), Locale.forLanguageTag(languageTag)); assertTrue(html.contains(""), "page was truncated before "); assertTrue(html.contains("let watermarkTxt = '';"), "watermarkTxt did not fall back to an empty string"); - assertTrue(html.contains("watermark_width: 240,"), "watermarkWidth did not fall back to its declared default"); - assertTrue(html.contains("watermark_font: '\u5fae\u8f6f\u96c5\u9ed1',"), "watermarkFont did not fall back to its declared default"); + assertRendered(html, "watermark_x_space: 10,"); + assertRendered(html, "watermark_y_space: 10,"); + assertRendered(html, "watermark_font: '微软雅黑',"); + assertRendered(html, "watermark_fontsize: '18px',"); + assertRendered(html, "watermark_color: 'black',"); + assertRendered(html, "watermark_alpha: 0.2,"); + assertRendered(html, "watermark_width: 240,"); + assertRendered(html, "watermark_height: 80,"); + assertRendered(html, "watermark_angle: 10,"); } - @Test - void shouldKeepConfiguredWatermarkSettingsWhenAttributesArePresent() throws Exception { + @ParameterizedTest + @ValueSource(strings = {"zh-CN", "de-DE", "ar-EG"}) + void shouldKeepConfiguredWatermarkSettingsWhenAttributesArePresent(String languageTag) throws Exception { Map model = baseModel(); model.put("watermarkTxt", "kkFileView"); model.put("watermarkXSpace", "20"); @@ -51,18 +68,26 @@ void shouldKeepConfiguredWatermarkSettingsWhenAttributesArePresent() throws Exce model.put("watermarkHeight", "120"); model.put("watermarkAngle", "45"); - String html = render(model); + String html = render(model, Locale.forLanguageTag(languageTag)); assertTrue(html.contains("let watermarkTxt = 'kkFileView';")); - assertTrue(html.contains("watermark_x_space: 20,")); - assertTrue(html.contains("watermark_y_space: 30,")); - assertTrue(html.contains("watermark_font: 'Arial',")); - assertTrue(html.contains("watermark_fontsize: '24px',")); - assertTrue(html.contains("watermark_color: 'red',")); - assertTrue(html.contains("watermark_alpha: 0.5,")); - assertTrue(html.contains("watermark_width: 300,")); - assertTrue(html.contains("watermark_height: 120,")); - assertTrue(html.contains("watermark_angle: 45,")); + assertRendered(html, "watermark_x_space: 20,"); + assertRendered(html, "watermark_y_space: 30,"); + assertRendered(html, "watermark_font: 'Arial',"); + assertRendered(html, "watermark_fontsize: '24px',"); + assertRendered(html, "watermark_color: 'red',"); + assertRendered(html, "watermark_alpha: 0.5,"); + assertRendered(html, "watermark_width: 300,"); + assertRendered(html, "watermark_height: 120,"); + assertRendered(html, "watermark_angle: 45,"); + } + + /** On failure, show the line that was actually emitted for that key. */ + private static void assertRendered(String html, String expected) { + String key = expected.substring(0, expected.indexOf(':') + 1); + int start = html.indexOf(key); + String actual = start < 0 ? "" : html.substring(start, html.indexOf('\n', start)).trim(); + assertTrue(html.contains(expected), "expected \"" + expected + "\" but rendered \"" + actual + "\""); } /** Everything officeweb.ftl needs except the watermark attributes. */ @@ -78,13 +103,14 @@ private Map baseModel() { return model; } - private String render(Map model) throws Exception { + /** Same lookup as Spring's FreeMarkerView: the template is resolved for the request locale. */ + private String render(Map model, Locale locale) throws Exception { Configuration configuration = new Configuration(Configuration.VERSION_2_3_32); configuration.setClassLoaderForTemplateLoading(getClass().getClassLoader(), "web"); configuration.setDefaultEncoding("UTF-8"); configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); - Template template = configuration.getTemplate("officeweb.ftl"); + Template template = configuration.getTemplate("officeweb.ftl", locale); StringWriter out = new StringWriter(); template.process(model, out); return out.toString();