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

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.20.x

#### What this PR does / why we need it:
模板 head 和 footer 标签注入功能忽略错误页面避免当扩展发生错误时导致错误页面无法显示

#### Which issue(s) this PR fixes:
Fixes #6500 , #6750

#### Does this PR introduce a user-facing change?
```release-note
代码注入功能忽略对错误页面和登录注册等页面的注入
```
pull/6817/head
guqing 2024-10-10 17:45:01 +08:00 committed by GitHub
parent 53b3124288
commit 02c54846dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 161 additions and 0 deletions

View File

@ -42,6 +42,9 @@ public class GlobalHeadInjectionProcessor extends AbstractElementModelProcessor
@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);

View File

@ -41,6 +41,7 @@ public class HaloProcessorDialect extends AbstractProcessorDialect
processors.add(new EvaluationContextEnhancer());
processors.add(new CommentElementTagProcessor(dialectPrefix));
processors.add(new CommentEnabledVariableProcessor());
processors.add(new InjectionExcluderProcessor());
return processors;
}

View File

@ -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;
}
}
}

View File

@ -48,6 +48,10 @@ public class TemplateFooterElementTagProcessor extends AbstractElementTagProcess
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.

View File

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