mirror of https://github.com/halo-dev/halo
Fix the problem of not using error template in theme (#3166)
#### 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 https://github.com/halo-dev/halo/issues/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 解决主题自定义错误模板不生效的问题。 ```pull/3168/head
parent
da07d75df3
commit
2241c08371
|
@ -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;
|
||||
|
@ -11,9 +12,16 @@ import org.springframework.http.ProblemDetail;
|
|||
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.
|
||||
*
|
||||
|
@ -29,6 +37,9 @@ public class HaloErrorWebExceptionHandler extends DefaultErrorWebExceptionHandle
|
|||
ErrorProperties errorProperties,
|
||||
ApplicationContext applicationContext) {
|
||||
super(errorAttributes, resources, errorProperties, applicationContext);
|
||||
this.templateAvailabilityProvider =
|
||||
applicationContext.getBean(ThemeTemplateAvailabilityProvider.class);
|
||||
this.themeResolver = applicationContext.getBean(ThemeResolver.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -45,4 +56,24 @@ public class HaloErrorWebExceptionHandler extends DefaultErrorWebExceptionHandle
|
|||
.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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import org.thymeleaf.util.StringUtils;
|
||||
|
||||
/**
|
||||
* File system path utils.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class FilePathUtils {
|
||||
private FilePathUtils() {
|
||||
}
|
||||
|
||||
public static Path combinePath(String first, String... more) {
|
||||
FileSystem fileSystem = FileSystems.getDefault();
|
||||
Path path = fileSystem.getPath(first, more);
|
||||
String unixPath = StringUtils.replace(path.normalize(), "\\", "/");
|
||||
return Paths.get(unixPath);
|
||||
}
|
||||
}
|
|
@ -100,7 +100,7 @@ public class TemplateEngineManager {
|
|||
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() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
|
|||
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;
|
||||
|
@ -18,8 +19,7 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
|||
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;
|
||||
|
||||
|
@ -29,10 +29,11 @@ import run.halo.app.theme.dialect.LinkExpressionObjectDialect;
|
|||
*/
|
||||
@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
|
||||
|
@ -43,6 +44,7 @@ public class ThemeConfiguration {
|
|||
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());
|
||||
|
@ -61,8 +63,11 @@ public class ThemeConfiguration {
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
@ -20,17 +17,14 @@ import run.halo.app.infra.utils.FilePathUtils;
|
|||
@Component
|
||||
@AllArgsConstructor
|
||||
public class ThemeResolver {
|
||||
private static final String THEME_WORK_DIR = "themes";
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
private final HaloProperties haloProperties;
|
||||
|
||||
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)
|
||||
|
@ -55,8 +49,7 @@ public class ThemeResolver {
|
|||
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)
|
||||
|
@ -64,22 +57,4 @@ public class ThemeResolver {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue