Skip to content

Commit

Permalink
Fix the problem of not using error template in theme (#3166)
Browse files Browse the repository at this point in the history
#### What type of PR is this?

/kind bug
/area core

#### What this PR does / why we need it:

Currently, if there is no `error.html` error template in theme, but there is a `404.html` error template, this will not work correctly.

We always get rendering result from global error template `error.html`.

This PR mainly provides a `ThemeTemplateAvailabilityProvider` to check if the template is available in theme instead of in globally predefined templates.

#### Which issue(s) this PR fixes:

Fixes #3062

#### Special notes for your reviewer:

1. Download and install any theme
2. Check the theme folder
3. Check folder `templates/error`
4. Try to remove `templates/error/error.html` template file
5. Create `templates/error/404.html`
6. Request a page which does not exist
7. See the result

#### Does this PR introduce a user-facing change?

```release-note
解决主题自定义错误模板不生效的问题。
```
  • Loading branch information
JohnNiang authored Jan 19, 2023
1 parent da07d75 commit 2241c08
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 63 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package run.halo.app.infra.exception.handlers;

import java.util.Map;
import java.util.Optional;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
Expand All @@ -11,9 +12,16 @@
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.theme.ThemeContext;
import run.halo.app.theme.ThemeResolver;
import run.halo.app.theme.engine.ThemeTemplateAvailabilityProvider;

public class HaloErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {

private final ThemeTemplateAvailabilityProvider templateAvailabilityProvider;

private final ThemeResolver themeResolver;

/**
* Create a new {@code DefaultErrorWebExceptionHandler} instance.
*
Expand All @@ -29,6 +37,9 @@ public HaloErrorWebExceptionHandler(
ErrorProperties errorProperties,
ApplicationContext applicationContext) {
super(errorAttributes, resources, errorProperties, applicationContext);
this.templateAvailabilityProvider =
applicationContext.getBean(ThemeTemplateAvailabilityProvider.class);
this.themeResolver = applicationContext.getBean(ThemeResolver.class);
}

@Override
Expand All @@ -45,4 +56,24 @@ protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.bodyValue(errorAttributes.get("error"));
}

@Override
protected Mono<ServerResponse> renderErrorView(ServerRequest request) {
return themeResolver.getTheme(request.exchange().getRequest())
.flatMap(themeContext -> super.renderErrorView(request)
.contextWrite(context -> context.put(ThemeContext.class, themeContext)));
}

@Override
protected Mono<ServerResponse> renderErrorView(String viewName,
ServerResponse.BodyBuilder responseBody, Map<String, Object> error) {
return Mono.deferContextual(contextView -> {
Optional<ThemeContext> themeContext = contextView.getOrEmpty(ThemeContext.class);
if (themeContext.isPresent()
&& templateAvailabilityProvider.isTemplateAvailable(themeContext.get(), viewName)) {
return responseBody.render(viewName, error);
}
return super.renderErrorView(viewName, responseBody, error);
});
}
}
25 changes: 0 additions & 25 deletions src/main/java/run/halo/app/infra/utils/FilePathUtils.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ private ISpringWebFluxTemplateEngine templateEngineGenerator(ThemeContext theme)
thymeleafProperties.isRenderHiddenMarkersBeforeCheckboxes());

var mainResolver = haloTemplateResolver();
mainResolver.setPrefix(theme.getPath() + "/templates/");
mainResolver.setPrefix(theme.getPath().resolve("templates") + "/");
engine.addTemplateResolver(mainResolver);
// replace StandardDialect with SpringStandardDialect
engine.setDialect(new SpringStandardDialect() {
Expand Down
19 changes: 12 additions & 7 deletions src/main/java/run/halo/app/theme/ThemeConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -18,8 +19,7 @@
import org.springframework.web.reactive.function.server.ServerResponse;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
import reactor.core.publisher.Mono;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.utils.FilePathUtils;
import run.halo.app.infra.ThemeRootGetter;
import run.halo.app.theme.dialect.HaloSpringSecurityDialect;
import run.halo.app.theme.dialect.LinkExpressionObjectDialect;

Expand All @@ -29,10 +29,11 @@
*/
@Configuration
public class ThemeConfiguration {
private final HaloProperties haloProperties;

public ThemeConfiguration(HaloProperties haloProperties) {
this.haloProperties = haloProperties;
private final ThemeRootGetter themeRoot;

public ThemeConfiguration(ThemeRootGetter themeRoot) {
this.themeRoot = themeRoot;
}

@Bean
Expand All @@ -43,6 +44,7 @@ public RouterFunction<ServerResponse> themeAssets(WebProperties webProperties) {
request -> {
var themeName = request.pathVariable("themeName");
var resource = request.pathVariable("resource");
resource = StringUtils.removeStart(resource, "/");
var fsRes = new FileSystemResource(getThemeAssetsPath(themeName, resource));
var bodyBuilder = ServerResponse.ok()
.cacheControl(cacheProperties.getCachecontrol().toHttpCacheControl());
Expand All @@ -61,8 +63,11 @@ public RouterFunction<ServerResponse> themeAssets(WebProperties webProperties) {
}

private Path getThemeAssetsPath(String themeName, String resource) {
return FilePathUtils.combinePath(haloProperties.getWorkDir().toString(),
"themes", themeName, "templates", "assets", resource);
return themeRoot.get()
.resolve(themeName)
.resolve("templates")
.resolve("assets")
.resolve(resource);
}

@Bean
Expand Down
35 changes: 5 additions & 30 deletions src/main/java/run/halo/app/theme/ThemeResolver.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
package run.halo.app.theme;

import java.nio.file.Files;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import reactor.core.publisher.Mono;
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
import run.halo.app.infra.SystemSetting.Theme;
import run.halo.app.infra.properties.HaloProperties;
import run.halo.app.infra.utils.FilePathUtils;
import run.halo.app.infra.ThemeRootGetter;

/**
* @author johnniang
Expand All @@ -20,17 +17,14 @@
@Component
@AllArgsConstructor
public class ThemeResolver {
private static final String THEME_WORK_DIR = "themes";
private final SystemConfigurableEnvironmentFetcher environmentFetcher;

private final HaloProperties haloProperties;
private final SystemConfigurableEnvironmentFetcher environmentFetcher;

private final ThymeleafProperties thymeleafProperties;
private final ThemeRootGetter themeRoot;

public Mono<ThemeContext> getThemeContext(String themeName) {
Assert.hasText(themeName, "Theme name cannot be empty");
var path = FilePathUtils.combinePath(haloProperties.getWorkDir().toString(),
THEME_WORK_DIR, themeName);
var path = themeRoot.get().resolve(themeName);
return Mono.just(ThemeContext.builder().name(themeName).path(path))
.flatMap(builder -> environmentFetcher.fetch(Theme.GROUP, Theme.class)
.mapNotNull(Theme::getActive)
Expand All @@ -55,31 +49,12 @@ public Mono<ThemeContext> getTheme(ServerHttpRequest request) {
themeName = activatedTheme;
}
boolean active = StringUtils.equals(activatedTheme, themeName);
var path = FilePathUtils.combinePath(haloProperties.getWorkDir().toString(),
THEME_WORK_DIR, themeName);
var path = themeRoot.get().resolve(themeName);
return builder.name(themeName)
.path(path)
.active(active)
.build();
});
}

/**
* Check whether the template file exists.
*
* @param viewName view name must not be blank
* @return if exists return true, otherwise return false
*/
public Mono<Boolean> isTemplateAvailable(ServerHttpRequest request, String viewName) {
return getTheme(request)
.map(themeContext -> {
String prefix = themeContext.getPath() + "/templates/";
String viewNameToUse = viewName;
if (!viewNameToUse.endsWith(thymeleafProperties.getSuffix())) {
viewNameToUse = viewNameToUse + thymeleafProperties.getSuffix();
}
return Files.exists(FilePathUtils.combinePath(prefix, viewNameToUse));
})
.onErrorResume(e -> Mono.just(false));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package run.halo.app.theme.engine;

import java.nio.file.Files;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.stereotype.Component;
import run.halo.app.theme.ThemeContext;

@Component
public class DefaultThemeTemplateAvailabilityProvider implements ThemeTemplateAvailabilityProvider {

private final ThymeleafProperties thymeleafProperties;

public DefaultThemeTemplateAvailabilityProvider(ThymeleafProperties thymeleafProperties) {
this.thymeleafProperties = thymeleafProperties;
}

@Override
public boolean isTemplateAvailable(ThemeContext themeContext, String viewName) {
var suffix = thymeleafProperties.getSuffix();
// Currently, we only support Path here.
var path = themeContext.getPath().resolve("templates").resolve(viewName + suffix);
return Files.exists(path);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package run.halo.app.theme.engine;

import run.halo.app.theme.ThemeContext;

public interface ThemeTemplateAvailabilityProvider {

boolean isTemplateAvailable(ThemeContext themeContext, String viewName);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package run.halo.app.theme.engine;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;

import java.io.FileNotFoundException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.util.ResourceUtils;
import run.halo.app.theme.ThemeContext;

@ExtendWith(MockitoExtension.class)
class DefaultThemeTemplateAvailabilityProviderTest {

@InjectMocks
DefaultThemeTemplateAvailabilityProvider provider;

@Mock
ThymeleafProperties thymeleafProperties;

@Test
void templateAvailableTest() throws FileNotFoundException, URISyntaxException {
var themeUrl = ResourceUtils.getURL("classpath:themes/default");
var themePath = Path.of(themeUrl.toURI());

when(thymeleafProperties.getSuffix()).thenReturn(".html");
var themeContext = ThemeContext.builder()
.name("default")
.path(themePath)
.build();
boolean templateAvailable = provider.isTemplateAvailable(themeContext, "fake");
assertFalse(templateAvailable);

templateAvailable = provider.isTemplateAvailable(themeContext, "index");
assertTrue(templateAvailable);

templateAvailable = provider.isTemplateAvailable(themeContext, "timezone");
assertTrue(templateAvailable);
}
}

0 comments on commit 2241c08

Please sign in to comment.