fix: incorrect scope for haloCommentEnabled template variable (#4385)

#### What type of PR is this?
/kind bug
/area core
/milestone 2.9.x

#### What this PR does / why we need it:
修复评论启用状态的主题模板变量名作用域不正确的问题

评论组件标签处理器只会在处理到 `<halo:comment/>` 自定义标签时被执行,而 haloCommentEnabled 状态是评论标签之前使用的那么此时值还没有被评论标签处理器填充所以取不到正确的值,目前的做法是在模板开始解析时填充 haloCommentEnabled 变量到 context,但这样存在的问题时无法判断页面是否使用了评论自定义标签即每个页面都会有这个变量,不过目前没有更好的办法去解决这样的问题。

how to test it?
在模板页面的任意位置使用 `${haloCommentEnabled}` 都能取到正确的值。

#### Which issue(s) this PR fixes:
Fixes #4378

#### Does this PR introduce a user-facing change?
```release-note
修复评论启用状态的主题模板变量名作用域不正确的问题
```
pull/4428/head
guqing 2023-08-15 18:04:12 +08:00 committed by GitHub
parent bce65c4947
commit bdb8d10ea0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 252 additions and 139 deletions

View File

@ -1,33 +1,21 @@
package run.halo.app.theme.dialect;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static org.apache.commons.lang3.BooleanUtils.isTrue;
import java.util.Optional;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.support.DefaultConversionService;
import org.thymeleaf.context.Contexts;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.context.IWebContext;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.AbstractElementTagProcessor;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.spring6.context.SpringContextUtils;
import org.thymeleaf.templatemode.TemplateMode;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
/**
* <p>Comment element tag processor.</p>
* <p>Replace the comment tag <code>&#x3C;halo:comment /&#x3E;</code> with the given content.</p>
*
* @author guqing
* @see CommentEnabledVariableProcessor
* @since 2.0.0
*/
public class CommentElementTagProcessor extends AbstractElementTagProcessor {
public static final String COMMENT_ENABLED_MODEL_ATTRIBUTE = "haloCommentEnabled";
private static final String TAG_NAME = "comment";
private static final int PRECEDENCE = 1000;
@ -51,49 +39,12 @@ public class CommentElementTagProcessor extends AbstractElementTagProcessor {
@Override
protected void doProcess(ITemplateContext context, IProcessableElementTag tag,
IElementTagStructureHandler structureHandler) {
getCommentWidget(context).ifPresentOrElse(commentWidget -> {
populateAllowCommentAttribute(context, true);
commentWidget.render(context, tag, structureHandler);
}, () -> {
populateAllowCommentAttribute(context, false);
var commentWidget = (CommentWidget) context.getVariable(
CommentEnabledVariableProcessor.COMMENT_WIDGET_OBJECT_VARIABLE);
if (commentWidget == null) {
structureHandler.replaceWith("", false);
});
}
static void populateAllowCommentAttribute(ITemplateContext context, boolean allowComment) {
if (Contexts.isWebContext(context)) {
IWebContext webContext = Contexts.asWebContext(context);
webContext.getExchange()
.setAttributeValue(COMMENT_ENABLED_MODEL_ATTRIBUTE, allowComment);
return;
}
}
static Optional<CommentWidget> getCommentWidget(ITemplateContext context) {
final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);
SystemConfigurableEnvironmentFetcher environmentFetcher =
appCtx.getBean(SystemConfigurableEnvironmentFetcher.class);
var commentSetting = environmentFetcher.fetchComment()
.blockOptional()
.orElseThrow();
var globalEnabled = isTrue(commentSetting.getEnable());
if (!globalEnabled) {
return Optional.empty();
}
if (Contexts.isWebContext(context)) {
IWebContext webContext = Contexts.asWebContext(context);
Object attributeValue = webContext.getExchange()
.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE);
Boolean enabled = DefaultConversionService.getSharedInstance()
.convert(attributeValue, Boolean.class);
if (isFalse(enabled)) {
return Optional.empty();
}
}
ExtensionGetter extensionGetter = appCtx.getBean(ExtensionGetter.class);
return extensionGetter.getEnabledExtensionByDefinition(CommentWidget.class)
.next()
.blockOptional();
commentWidget.render(context, tag, structureHandler);
}
}

View File

@ -0,0 +1,91 @@
package run.halo.app.theme.dialect;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
import static org.apache.commons.lang3.BooleanUtils.isTrue;
import java.util.Optional;
import org.springframework.context.ApplicationContext;
import org.springframework.core.convert.support.DefaultConversionService;
import org.thymeleaf.context.Contexts;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.context.IWebContext;
import org.thymeleaf.model.ITemplateEnd;
import org.thymeleaf.model.ITemplateStart;
import org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor;
import org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler;
import org.thymeleaf.spring6.context.SpringContextUtils;
import org.thymeleaf.standard.StandardDialect;
import org.thymeleaf.templatemode.TemplateMode;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
/**
* Comment enabled variable processor.
* <p>Compute comment enabled state and set it to the model when the template is start rendering</p>
* <p>It is not suitable for scenarios where there are multiple comment components on the same page
* and some of them need to be controlled to be closed.</p>
*
* @author guqing
* @since 2.9.0
*/
public class CommentEnabledVariableProcessor extends AbstractTemplateBoundariesProcessor {
public static final String COMMENT_WIDGET_OBJECT_VARIABLE = CommentWidget.class.getName();
public static final String COMMENT_ENABLED_MODEL_ATTRIBUTE = "haloCommentEnabled";
public CommentEnabledVariableProcessor() {
super(TemplateMode.HTML, StandardDialect.PROCESSOR_PRECEDENCE);
}
@Override
public void doProcessTemplateStart(ITemplateContext context, ITemplateStart templateStart,
ITemplateBoundariesStructureHandler structureHandler) {
getCommentWidget(context).ifPresentOrElse(commentWidget -> {
populateAllowCommentAttribute(context, true);
structureHandler.setLocalVariable(COMMENT_WIDGET_OBJECT_VARIABLE, commentWidget);
}, () -> populateAllowCommentAttribute(context, false));
}
@Override
public void doProcessTemplateEnd(ITemplateContext context, ITemplateEnd templateEnd,
ITemplateBoundariesStructureHandler structureHandler) {
structureHandler.removeLocalVariable(COMMENT_WIDGET_OBJECT_VARIABLE);
}
static void populateAllowCommentAttribute(ITemplateContext context, boolean allowComment) {
if (Contexts.isWebContext(context)) {
IWebContext webContext = Contexts.asWebContext(context);
webContext.getExchange()
.setAttributeValue(COMMENT_ENABLED_MODEL_ATTRIBUTE, allowComment);
}
}
static Optional<CommentWidget> getCommentWidget(ITemplateContext context) {
final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);
SystemConfigurableEnvironmentFetcher environmentFetcher =
appCtx.getBean(SystemConfigurableEnvironmentFetcher.class);
var commentSetting = environmentFetcher.fetchComment()
.blockOptional()
.orElseThrow();
var globalEnabled = isTrue(commentSetting.getEnable());
if (!globalEnabled) {
return Optional.empty();
}
if (Contexts.isWebContext(context)) {
IWebContext webContext = Contexts.asWebContext(context);
Object attributeValue = webContext.getExchange()
.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE);
Boolean enabled = DefaultConversionService.getSharedInstance()
.convert(attributeValue, Boolean.class);
if (isFalse(enabled)) {
return Optional.empty();
}
}
ExtensionGetter extensionGetter = appCtx.getBean(ExtensionGetter.class);
return extensionGetter.getEnabledExtensionByDefinition(CommentWidget.class)
.next()
.blockOptional();
}
}

View File

@ -35,6 +35,7 @@ public class HaloProcessorDialect extends AbstractProcessorDialect implements
processors.add(new TemplateFooterElementTagProcessor(dialectPrefix));
processors.add(new JsonNodePropertyAccessorBoundariesProcessor());
processors.add(new CommentElementTagProcessor(dialectPrefix));
processors.add(new CommentEnabledVariableProcessor());
return processors;
}

View File

@ -1,6 +1,8 @@
package run.halo.app.theme;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
@ -24,6 +26,8 @@ import org.thymeleaf.templateresource.ITemplateResource;
import org.thymeleaf.templateresource.StringTemplateResource;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.theme.dialect.HaloProcessorDialect;
@ -40,6 +44,9 @@ public class ReactiveFinderExpressionParserTests {
@Mock
private ApplicationContext applicationContext;
@Mock
private SystemConfigurableEnvironmentFetcher environmentFetcher;
private TemplateEngine templateEngine;
@BeforeEach
@ -53,6 +60,10 @@ public class ReactiveFinderExpressionParserTests {
}
}));
templateEngine.addTemplateResolver(new TestTemplateResolver());
lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
.thenReturn(environmentFetcher);
lenient().when(environmentFetcher.fetchComment())
.thenReturn(Mono.just(new SystemSetting.Comment()));
}
@Test

View File

@ -4,7 +4,6 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Map;
@ -19,7 +18,6 @@ import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.context.WebEngineContext;
import org.thymeleaf.model.IProcessableElementTag;
import org.thymeleaf.processor.element.IElementTagStructureHandler;
import org.thymeleaf.spring6.dialect.SpringStandardDialect;
@ -27,7 +25,6 @@ import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;
import org.thymeleaf.templateresolver.StringTemplateResolver;
import org.thymeleaf.templateresource.ITemplateResource;
import org.thymeleaf.templateresource.StringTemplateResource;
import org.thymeleaf.web.IWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
@ -105,74 +102,6 @@ class CommentElementTagProcessorTest {
""");
}
@Test
void getCommentWidget() {
when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
.thenReturn(environmentFetcher);
SystemSetting.Comment commentSetting = mock(SystemSetting.Comment.class);
when(environmentFetcher.fetchComment())
.thenReturn(Mono.just(commentSetting));
CommentWidget commentWidget = mock(CommentWidget.class);
when(extensionGetter.getEnabledExtensionByDefinition(CommentWidget.class))
.thenReturn(Flux.just(commentWidget));
WebEngineContext webContext = mock(WebEngineContext.class);
var evaluationContext = mock(ThymeleafEvaluationContext.class);
when(webContext.getVariable(
eq(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME)))
.thenReturn(evaluationContext);
when(evaluationContext.getApplicationContext()).thenReturn(applicationContext);
IWebExchange webExchange = mock(IWebExchange.class);
when(webContext.getExchange()).thenReturn(webExchange);
// comment disabled
when(commentSetting.getEnable()).thenReturn(true);
assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isTrue();
// comment enabled
when(commentSetting.getEnable()).thenReturn(false);
assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isFalse();
// comment enabled and ENABLE_COMMENT_ATTRIBUTE is true
when(commentSetting.getEnable()).thenReturn(true);
when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))
.thenReturn(true);
assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isTrue();
// comment enabled and ENABLE_COMMENT_ATTRIBUTE is false
when(commentSetting.getEnable()).thenReturn(true);
when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))
.thenReturn(false);
assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isFalse();
// comment enabled and ENABLE_COMMENT_ATTRIBUTE is null
when(commentSetting.getEnable()).thenReturn(true);
when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))
.thenReturn(null);
assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isTrue();
// comment enabled and ENABLE_COMMENT_ATTRIBUTE is 'false'
when(commentSetting.getEnable()).thenReturn(true);
when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))
.thenReturn("false");
assertThat(CommentElementTagProcessor.getCommentWidget(webContext).isPresent()).isFalse();
}
@Test
void populateAllowCommentAttribute() {
WebEngineContext webContext = mock(WebEngineContext.class);
IWebExchange webExchange = mock(IWebExchange.class);
when(webContext.getExchange()).thenReturn(webExchange);
CommentElementTagProcessor.populateAllowCommentAttribute(webContext, true);
verify(webExchange).setAttributeValue(
eq(CommentElementTagProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(true));
CommentElementTagProcessor.populateAllowCommentAttribute(webContext, false);
verify(webExchange).setAttributeValue(
eq(CommentElementTagProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(false));
}
static class DefaultCommentWidget implements CommentWidget {
@Override

View File

@ -0,0 +1,121 @@
package run.halo.app.theme.dialect;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.context.ApplicationContext;
import org.thymeleaf.context.WebEngineContext;
import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;
import org.thymeleaf.web.IWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
/**
* Tests for {@link CommentEnabledVariableProcessor}.
*
* @author guqing
* @since 2.9.0
*/
@ExtendWith(MockitoExtension.class)
class CommentEnabledVariableProcessorTest {
@Mock
private ApplicationContext applicationContext;
@Mock
private ExtensionGetter extensionGetter;
@Mock
private SystemConfigurableEnvironmentFetcher environmentFetcher;
@BeforeEach
void setUp() {
lenient().when(applicationContext.getBean(eq(ExtensionGetter.class)))
.thenReturn(extensionGetter);
}
@Test
void getCommentWidget() {
when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
.thenReturn(environmentFetcher);
SystemSetting.Comment commentSetting = mock(SystemSetting.Comment.class);
when(environmentFetcher.fetchComment())
.thenReturn(Mono.just(commentSetting));
CommentWidget commentWidget = mock(CommentWidget.class);
when(extensionGetter.getEnabledExtensionByDefinition(CommentWidget.class))
.thenReturn(Flux.just(commentWidget));
WebEngineContext webContext = mock(WebEngineContext.class);
var evaluationContext = mock(ThymeleafEvaluationContext.class);
when(webContext.getVariable(
eq(ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME)))
.thenReturn(evaluationContext);
when(evaluationContext.getApplicationContext()).thenReturn(applicationContext);
IWebExchange webExchange = mock(IWebExchange.class);
when(webContext.getExchange()).thenReturn(webExchange);
// comment disabled
when(commentSetting.getEnable()).thenReturn(true);
assertThat(
CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue();
// comment enabled
when(commentSetting.getEnable()).thenReturn(false);
assertThat(
CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse();
// comment enabled and ENABLE_COMMENT_ATTRIBUTE is true
when(commentSetting.getEnable()).thenReturn(true);
when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))
.thenReturn(true);
assertThat(
CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue();
// comment enabled and ENABLE_COMMENT_ATTRIBUTE is false
when(commentSetting.getEnable()).thenReturn(true);
when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))
.thenReturn(false);
assertThat(
CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse();
// comment enabled and ENABLE_COMMENT_ATTRIBUTE is null
when(commentSetting.getEnable()).thenReturn(true);
when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))
.thenReturn(null);
assertThat(
CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isTrue();
// comment enabled and ENABLE_COMMENT_ATTRIBUTE is 'false'
when(commentSetting.getEnable()).thenReturn(true);
when(webExchange.getAttributeValue(CommentWidget.ENABLE_COMMENT_ATTRIBUTE))
.thenReturn("false");
assertThat(
CommentEnabledVariableProcessor.getCommentWidget(webContext).isPresent()).isFalse();
}
@Test
void populateAllowCommentAttribute() {
WebEngineContext webContext = mock(WebEngineContext.class);
IWebExchange webExchange = mock(IWebExchange.class);
when(webContext.getExchange()).thenReturn(webExchange);
CommentEnabledVariableProcessor.populateAllowCommentAttribute(webContext, true);
verify(webExchange).setAttributeValue(
eq(CommentEnabledVariableProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(true));
CommentEnabledVariableProcessor.populateAllowCommentAttribute(webContext, false);
verify(webExchange).setAttributeValue(
eq(CommentEnabledVariableProcessor.COMMENT_ENABLED_MODEL_ATTRIBUTE), eq(false));
}
}

View File

@ -108,6 +108,10 @@ class ContentTemplateHeadProcessorIntegrationTest {
lenient().when(extensionComponentsFinder.getExtensions(eq(TemplateHeadProcessor.class)))
.thenReturn(new ArrayList<>(map.values()));
lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
.thenReturn(fetcher);
lenient().when(fetcher.fetchComment()).thenReturn(Mono.just(new SystemSetting.Comment()));
}
@ -137,19 +141,19 @@ class ContentTemplateHeadProcessorIntegrationTest {
3. but global head meta is not overridden by global seo meta
*/
assertThat(Jsoup.parse(result).html()).isEqualTo("""
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Post detail</title>
<meta name="description" content="post-description">
<meta name="keyword" content="postK1,postK2">
<meta name="other" content="post-other-meta">
</head>
<body>
this is body
</body>
</html>""");
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Post detail</title>
<meta name="description" content="post-description">
<meta name="keyword" content="postK1,postK2">
<meta name="other" content="post-other-meta">
</head>
<body>
this is body
</body>
</html>""");
}
Map<String, String> mutableMetaMap(String nameValue, String contentValue) {

View File

@ -104,6 +104,11 @@ class HaloProcessorDialectTest {
lenient().when(extensionComponentsFinder.getExtensions(eq(TemplateHeadProcessor.class)))
.thenReturn(new ArrayList<>(map.values()));
lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
.thenReturn(fetcher);
lenient().when(fetcher.fetchComment())
.thenReturn(Mono.just(new SystemSetting.Comment()));
}
@Test