Skip to content

Commit

Permalink
Add generator meta into head (#4821)
Browse files Browse the repository at this point in the history
#### 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

<meta name="generator" content="Halo 2.11.0-SNAPSHOT"/>
```

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 元数据标识
```
  • Loading branch information
JohnNiang committed Nov 6, 2023
1 parent caa4d44 commit 4ea2014
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 88 deletions.
Expand Up @@ -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 {

Expand Down
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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> buildProperties) {
return new GeneratorMetaProcessor(buildProperties);
}
}
@@ -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> buildProperties) {
this.generatorValue = "Halo " + buildProperties.stream().findFirst()
.map(BuildProperties::getVersion)
.orElse("Unknown");
}

@Override
public Mono<Void> 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);
});
}

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

}
Expand Up @@ -68,21 +68,9 @@ void messageResolverWhenDefaultTheme() {
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
index
<div>zh</div>
<div>欢迎来到首页</div>
</body>
</html>
""");
.expectBody()
.xpath("/html/body/div[1]").isEqualTo("zh")
.xpath("/html/body/div[2]").isEqualTo("欢迎来到首页");
}

@Test
Expand All @@ -92,21 +80,9 @@ void messageResolverForEnLanguageWhenDefaultTheme() {
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
index
<div>en</div>
<div>Welcome to the index</div>
</body>
</html>
""");
.expectBody()
.xpath("/html/body/div[1]").isEqualTo("en")
.xpath("/html/body/div[2]").isEqualTo("Welcome to the index");
}

@Test
Expand All @@ -116,21 +92,9 @@ void shouldUseDefaultWhenLanguageNotSupport() {
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
index
<div>foo</div>
<div>欢迎来到首页</div>
</body>
</html>
""");
.expectBody()
.xpath("/html/body/div[1]").isEqualTo("foo")
.xpath("/html/body/div[2]").isEqualTo("欢迎来到首页");
}

@Test
Expand All @@ -140,57 +104,28 @@ void switchTheme() throws URISyntaxException {
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.isEqualTo("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
index
<div>zh</div>
<div>欢迎来到首页</div>
</body>
</html>
""");
.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)))
.thenReturn(Mono.just(createOtherContext()));
webTestClient.get()
.uri("/index?language=zh")
.exchange()
.expectBody(String.class)
.isEqualTo("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Other theme title</title>
</head>
<body>
<p>Other 首页</p>
</body>
</html>
""");
.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("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Other theme title</title>
</head>
<body>
<p>other index</p>
</body>
</html>
""");
.expectBody()
.xpath("/html/head/title").isEqualTo("Other theme title")
.xpath("/html/body/p").isEqualTo("other index");
}

ThemeContext createDefaultContext() throws URISyntaxException {
Expand Down
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8"/>
<title>Title</title>
</head>
<body>
Expand Down
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8"/>
<title>Other theme title</title>
</head>
<body>
Expand Down

0 comments on commit 4ea2014

Please sign in to comment.