From 4ea20142f5b03c7e6515975670f729061659160f Mon Sep 17 00:00:00 2001 From: John Niang Date: Mon, 6 Nov 2023 10:52:48 +0800 Subject: [PATCH] Add generator meta into head (#4821) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /milestone 2.11.x #### What this PR does / why we need it: Please see https://html.spec.whatwg.org/multipage/semantics.html#meta-generator for more. This PR add the generator meta into head, so that we can know what sites are using Halo. ```bash http localhost:8090/ | grep generator ``` If someone want to disable the generator meta, they can configure the property `halo.theme.generator-meta-disabled` to `true`. #### Does this PR introduce a user-facing change? ```release-note 添加 Generator 元数据标识 ``` --- .../app/infra/properties/ThemeProperties.java | 5 + .../halo/app/theme/ThemeConfiguration.java | 13 +++ .../theme/dialect/GeneratorMetaProcessor.java | 43 +++++++ .../dialect/GeneratorMetaProcessorTest.java | 59 ++++++++++ .../ThemeMessageResolverIntegrationTest.java | 107 ++++-------------- .../themes/default/templates/index.html | 2 +- .../themes/other/templates/index.html | 2 +- 7 files changed, 143 insertions(+), 88 deletions(-) create mode 100644 application/src/main/java/run/halo/app/theme/dialect/GeneratorMetaProcessor.java create mode 100644 application/src/test/java/run/halo/app/theme/dialect/GeneratorMetaProcessorTest.java diff --git a/application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java b/application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java index b4decf3f34..7f5f11c911 100644 --- a/application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java +++ b/application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java @@ -9,6 +9,11 @@ public class ThemeProperties { @Valid private final Initializer initializer = new Initializer(); + /** + * Indicates whether the generator meta needs to be disabled. + */ + private boolean generatorMetaDisabled; + @Data public static class Initializer { diff --git a/application/src/main/java/run/halo/app/theme/ThemeConfiguration.java b/application/src/main/java/run/halo/app/theme/ThemeConfiguration.java index 1a921761b6..08e81cbebb 100644 --- a/application/src/main/java/run/halo/app/theme/ThemeConfiguration.java +++ b/application/src/main/java/run/halo/app/theme/ThemeConfiguration.java @@ -8,7 +8,10 @@ import java.nio.file.Path; import java.time.Instant; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.boot.info.BuildProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.FileSystemResource; @@ -21,8 +24,10 @@ import reactor.core.publisher.Mono; import run.halo.app.infra.ThemeRootGetter; import run.halo.app.infra.utils.FileUtils; +import run.halo.app.theme.dialect.GeneratorMetaProcessor; import run.halo.app.theme.dialect.HaloSpringSecurityDialect; import run.halo.app.theme.dialect.LinkExpressionObjectDialect; +import run.halo.app.theme.dialect.TemplateHeadProcessor; /** * @author guqing @@ -86,4 +91,12 @@ SpringSecurityDialect springSecurityDialect( ServerSecurityContextRepository securityContextRepository) { return new HaloSpringSecurityDialect(securityContextRepository); } + + @Bean + @ConditionalOnProperty(name = "halo.theme.generator-meta-disabled", + havingValue = "false", + matchIfMissing = true) + TemplateHeadProcessor generatorMetaProcessor(ObjectProvider buildProperties) { + return new GeneratorMetaProcessor(buildProperties); + } } diff --git a/application/src/main/java/run/halo/app/theme/dialect/GeneratorMetaProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/GeneratorMetaProcessor.java new file mode 100644 index 0000000000..ca2ee56cd5 --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/dialect/GeneratorMetaProcessor.java @@ -0,0 +1,43 @@ +package run.halo.app.theme.dialect; + +import static org.thymeleaf.model.AttributeValueQuotes.DOUBLE; + +import java.util.Map; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.info.BuildProperties; +import org.springframework.core.annotation.Order; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.model.IModel; +import org.thymeleaf.processor.element.IElementModelStructureHandler; +import reactor.core.publisher.Mono; + +/** + * Processor for generating generator meta. + * Set the order to 0 for removing the meta in later TemplateHeadProcessor. + * + * @author johnniang + */ +@Order(0) +public class GeneratorMetaProcessor implements TemplateHeadProcessor { + + private final String generatorValue; + + public GeneratorMetaProcessor(ObjectProvider buildProperties) { + this.generatorValue = "Halo " + buildProperties.stream().findFirst() + .map(BuildProperties::getVersion) + .orElse("Unknown"); + } + + @Override + public Mono process(ITemplateContext context, IModel model, + IElementModelStructureHandler structureHandler) { + return Mono.fromRunnable(() -> { + var modelFactory = context.getModelFactory(); + var generatorMeta = modelFactory.createStandaloneElementTag("meta", + Map.of("name", "generator", "content", generatorValue), + DOUBLE, false, true); + model.add(generatorMeta); + }); + } + +} diff --git a/application/src/test/java/run/halo/app/theme/dialect/GeneratorMetaProcessorTest.java b/application/src/test/java/run/halo/app/theme/dialect/GeneratorMetaProcessorTest.java new file mode 100644 index 0000000000..a732d7aecd --- /dev/null +++ b/application/src/test/java/run/halo/app/theme/dialect/GeneratorMetaProcessorTest.java @@ -0,0 +1,59 @@ +package run.halo.app.theme.dialect; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import java.io.FileNotFoundException; +import java.net.URISyntaxException; +import java.nio.file.Path; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.ResourceUtils; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; +import run.halo.app.infra.InitializationStateGetter; +import run.halo.app.theme.ThemeContext; +import run.halo.app.theme.ThemeResolver; + +@SpringBootTest +@AutoConfigureWebTestClient +class GeneratorMetaProcessorTest { + + @Autowired + WebTestClient webClient; + + @MockBean + InitializationStateGetter initializationStateGetter; + + @MockBean + ThemeResolver themeResolver; + + @BeforeEach + void setUp() throws FileNotFoundException, URISyntaxException { + when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true)); + var themeContext = ThemeContext.builder() + .name("default") + .path(Path.of(ResourceUtils.getURL("classpath:themes/default").toURI())) + .active(true) + .build(); + when(themeResolver.getTheme(any(ServerWebExchange.class))) + .thenReturn(Mono.just(themeContext)); + } + + @Test + void requestIndexPage() { + webClient.get().uri("/") + .exchange() + .expectStatus().isOk() + .expectBody() + .consumeWith(System.out::println) + .xpath("/html/head/meta[@name=\"generator\"][starts-with(@content, \"Halo \")]") + .exists(); + } + +} diff --git a/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java b/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java index dc63f6b44d..6948c4e96c 100644 --- a/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java +++ b/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java @@ -68,21 +68,9 @@ void messageResolverWhenDefaultTheme() { .exchange() .expectStatus() .isOk() - .expectBody(String.class) - .isEqualTo(""" - - - - - Title - - - index -
zh
-
欢迎来到首页
- - - """); + .expectBody() + .xpath("/html/body/div[1]").isEqualTo("zh") + .xpath("/html/body/div[2]").isEqualTo("欢迎来到首页"); } @Test @@ -92,21 +80,9 @@ void messageResolverForEnLanguageWhenDefaultTheme() { .exchange() .expectStatus() .isOk() - .expectBody(String.class) - .isEqualTo(""" - - - - - Title - - - index -
en
-
Welcome to the index
- - - """); + .expectBody() + .xpath("/html/body/div[1]").isEqualTo("en") + .xpath("/html/body/div[2]").isEqualTo("Welcome to the index"); } @Test @@ -116,21 +92,9 @@ void shouldUseDefaultWhenLanguageNotSupport() { .exchange() .expectStatus() .isOk() - .expectBody(String.class) - .isEqualTo(""" - - - - - Title - - - index -
foo
-
欢迎来到首页
- - - """); + .expectBody() + .xpath("/html/body/div[1]").isEqualTo("foo") + .xpath("/html/body/div[2]").isEqualTo("欢迎来到首页"); } @Test @@ -140,21 +104,11 @@ void switchTheme() throws URISyntaxException { .exchange() .expectStatus() .isOk() - .expectBody(String.class) - .isEqualTo(""" - - - - - Title - - - index -
zh
-
欢迎来到首页
- - - """); + .expectBody() + .xpath("/html/head/title").isEqualTo("Title") + .xpath("/html/body/div[1]").isEqualTo("zh") + .xpath("/html/body/div[2]").isEqualTo("欢迎来到首页") + ; // For other theme when(themeResolver.getTheme(any(ServerWebExchange.class))) @@ -162,35 +116,16 @@ void switchTheme() throws URISyntaxException { webTestClient.get() .uri("/index?language=zh") .exchange() - .expectBody(String.class) - .isEqualTo(""" - - - - - Other theme title - - -

Other 首页

- - - """); + .expectBody() + .xpath("/html/head/title").isEqualTo("Other theme title") + .xpath("/html/body/p").isEqualTo("Other 首页"); + webTestClient.get() .uri("/index?language=en") .exchange() - .expectBody(String.class) - .isEqualTo(""" - - - - - Other theme title - - -

other index

- - - """); + .expectBody() + .xpath("/html/head/title").isEqualTo("Other theme title") + .xpath("/html/body/p").isEqualTo("other index"); } ThemeContext createDefaultContext() throws URISyntaxException { diff --git a/application/src/test/resources/themes/default/templates/index.html b/application/src/test/resources/themes/default/templates/index.html index 441ad470cf..7d38411c43 100644 --- a/application/src/test/resources/themes/default/templates/index.html +++ b/application/src/test/resources/themes/default/templates/index.html @@ -1,7 +1,7 @@ - + Title diff --git a/application/src/test/resources/themes/other/templates/index.html b/application/src/test/resources/themes/other/templates/index.html index ca476f69cd..2ecf09eafa 100644 --- a/application/src/test/resources/themes/other/templates/index.html +++ b/application/src/test/resources/themes/other/templates/index.html @@ -1,7 +1,7 @@ - + Other theme title