mirror of https://github.com/halo-dev/halo
				
				
				
			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
							parent
							
								
									53b3124288
								
							
						
					
					
						commit
						02c54846dc
					
				| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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…
	
		Reference in New Issue