Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: head and footer tag injection to skip error pages #6709

Merged
merged 2 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,91 @@
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/.*")
);

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,62 @@
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"
};

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