diff --git a/application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java index a346d42e0b..ee741a887a 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java +++ b/application/src/main/java/run/halo/app/theme/dialect/GlobalHeadInjectionProcessor.java @@ -42,6 +42,9 @@ public GlobalHeadInjectionProcessor(final String dialectPrefix) { @Override protected void doProcess(ITemplateContext context, IModel model, IElementModelStructureHandler structureHandler) { + if (context.containsVariable(InjectionExcluderProcessor.EXCLUDE_INJECTION_VARIABLE)) { + return; + } // note that this is important!! Object processedAlready = context.getVariable(PROCESS_FLAG); diff --git a/application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java b/application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java index a355f5ae88..99105931d7 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java +++ b/application/src/main/java/run/halo/app/theme/dialect/HaloProcessorDialect.java @@ -41,6 +41,7 @@ public Set getProcessors(String dialectPrefix) { processors.add(new EvaluationContextEnhancer()); processors.add(new CommentElementTagProcessor(dialectPrefix)); processors.add(new CommentEnabledVariableProcessor()); + processors.add(new InjectionExcluderProcessor()); return processors; } diff --git a/application/src/main/java/run/halo/app/theme/dialect/InjectionExcluderProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/InjectionExcluderProcessor.java new file mode 100644 index 0000000000..ab0fe4d199 --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/dialect/InjectionExcluderProcessor.java @@ -0,0 +1,92 @@ +package run.halo.app.theme.dialect; + +import java.util.Set; +import java.util.regex.Pattern; +import org.springframework.util.Assert; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.model.ITemplateEnd; +import org.thymeleaf.model.ITemplateStart; +import org.thymeleaf.processor.templateboundaries.AbstractTemplateBoundariesProcessor; +import org.thymeleaf.processor.templateboundaries.ITemplateBoundariesProcessor; +import org.thymeleaf.processor.templateboundaries.ITemplateBoundariesStructureHandler; +import org.thymeleaf.standard.StandardDialect; +import org.thymeleaf.templatemode.TemplateMode; + +/** + *

Determine whether the current template being rendered needs to exclude the processor of + * code injection. If it needs to be excluded, set a local variable.

+ *

Why do you need to set a local variable here instead of directly judging in the processor?

+ *

Because the processor will process the fragment, and if you need to exclude the login + * .html template and the login.html is only a fragment, then the exclusion logic will + * fail, so here use {@link ITemplateBoundariesProcessor} events are only fired for the + * first-level template to solve this problem.

+ * + * @author guqing + * @since 2.20.0 + */ +public class InjectionExcluderProcessor extends AbstractTemplateBoundariesProcessor { + + public static final String EXCLUDE_INJECTION_VARIABLE = + InjectionExcluderProcessor.class.getName() + ".EXCLUDE_INJECTION"; + + private final PageInjectionExcluder injectionExcluder = new PageInjectionExcluder(); + + public InjectionExcluderProcessor() { + super(TemplateMode.HTML, StandardDialect.PROCESSOR_PRECEDENCE); + } + + @Override + public void doProcessTemplateStart(ITemplateContext context, ITemplateStart templateStart, + ITemplateBoundariesStructureHandler structureHandler) { + if (isExcluded(context)) { + structureHandler.setLocalVariable(EXCLUDE_INJECTION_VARIABLE, true); + } + } + + @Override + public void doProcessTemplateEnd(ITemplateContext context, ITemplateEnd templateEnd, + ITemplateBoundariesStructureHandler structureHandler) { + structureHandler.removeLocalVariable(EXCLUDE_INJECTION_VARIABLE); + } + + /** + * Check if the template will be rendered is excluded injection. + * + * @param context template context + * @return true if the template is excluded, otherwise false + */ + boolean isExcluded(ITemplateContext context) { + return injectionExcluder.isExcluded(context.getTemplateData().getTemplate()); + } + + static class PageInjectionExcluder { + + private final Set exactMatches = Set.of( + "login", + "signup", + "logout" + ); + + private final Set regexPatterns = Set.of( + Pattern.compile("error/.*"), + Pattern.compile("challenges/.*"), + Pattern.compile("password-reset/.*"), + Pattern.compile("login_.*") + ); + + public boolean isExcluded(String templateName) { + Assert.notNull(templateName, "Template name must not be null"); + if (exactMatches.contains(templateName)) { + return true; + } + + for (Pattern pattern : regexPatterns) { + if (pattern.matcher(templateName).matches()) { + return true; + } + } + + return false; + } + } +} diff --git a/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java index 26a286b26a..7177d2f789 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java +++ b/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java @@ -48,6 +48,10 @@ public TemplateFooterElementTagProcessor(final String dialectPrefix) { protected void doProcess(ITemplateContext context, IProcessableElementTag tag, IElementTagStructureHandler structureHandler) { + if (context.containsVariable(InjectionExcluderProcessor.EXCLUDE_INJECTION_VARIABLE)) { + return; + } + IModel modelToInsert = context.getModelFactory().createModel(); /* * Obtain the Spring application context. diff --git a/application/src/test/java/run/halo/app/theme/dialect/InjectionExcluderProcessorTest.java b/application/src/test/java/run/halo/app/theme/dialect/InjectionExcluderProcessorTest.java new file mode 100644 index 0000000000..e8b21d6532 --- /dev/null +++ b/application/src/test/java/run/halo/app/theme/dialect/InjectionExcluderProcessorTest.java @@ -0,0 +1,63 @@ +package run.halo.app.theme.dialect; + + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link InjectionExcluderProcessor}. + * + * @author guqing + * @since 2.20.0 + */ +class InjectionExcluderProcessorTest { + + @Nested + class PageInjectionExcluderTest { + final InjectionExcluderProcessor.PageInjectionExcluder pageInjectionExcluder = + new InjectionExcluderProcessor.PageInjectionExcluder(); + + @Test + void excludeTest() { + var cases = new String[] { + "login", + "signup", + "logout", + "password-reset/email/reset", + "error/404", + "error/500", + "challenges/totp", + "login_local" + }; + + for (String templateName : cases) { + assertThat(pageInjectionExcluder.isExcluded(templateName)).isTrue(); + } + } + + @Test + void shouldNotExcludeTest() { + var cases = new String[] { + "index", + "post", + "page", + "category", + "tag", + "archive", + "search", + "feed", + "sitemap", + "robots", + "custom", + "error", + "login.html", + }; + + for (String templateName : cases) { + assertThat(pageInjectionExcluder.isExcluded(templateName)).isFalse(); + } + } + } +} \ No newline at end of file