Skip to content

Commit

Permalink
refactor: head and footer tag injection to skip error pages
Browse files Browse the repository at this point in the history
  • Loading branch information
guqing committed Oct 10, 2024
1 parent 01a781c commit f8570e2
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public Set<IProcessor> getProcessors(String dialectPrefix) {
processors.add(new EvaluationContextEnhancer());
processors.add(new CommentElementTagProcessor(dialectPrefix));
processors.add(new CommentEnabledVariableProcessor());
processors.add(new InjectionExcluderProcessor());
return processors;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/**
* <p>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.</p>
* <p>Why do you need to set a local variable here instead of directly judging in the processor?</p>
* <p>Because the processor will process the fragment, and if you need to exclude the <code>login
* .html</code> 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.</p>
*
* @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<String> exactMatches = Set.of(
"login",
"signup",
"logout"
);

private final Set<Pattern> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}

0 comments on commit f8570e2

Please sign in to comment.