diff --git a/build.gradle b/build.gradle index 78db65fee..ad3fc73b3 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id "org.springframework.boot" version "2.4.2" + id "org.springframework.boot" version "2.5.0-M2" id "io.spring.dependency-management" version "1.0.11.RELEASE" id "checkstyle" id "java" @@ -8,11 +8,7 @@ plugins { group = "run.halo.app" version = "1.4.5" description = "Halo, An excellent open source blog publishing application." - -java { - archivesBaseName = "halo" - sourceCompatibility = JavaVersion.VERSION_11 -} +sourceCompatibility = JavaVersion.VERSION_11 checkstyle { toolVersion = "8.39" @@ -26,6 +22,7 @@ repositories { // url "https://maven.aliyun.com/nexus/content/groups/public" // } mavenCentral() + maven { url 'https://repo.spring.io/milestone' } jcenter() } @@ -111,7 +108,7 @@ dependencies { implementation "org.springframework.boot:spring-boot-starter-actuator" implementation "org.springframework.boot:spring-boot-starter-data-jpa" implementation "org.springframework.boot:spring-boot-starter-web" - implementation "org.springframework.boot:spring-boot-starter-jetty" + implementation "org.springframework.boot:spring-boot-starter-undertow" implementation "org.springframework.boot:spring-boot-starter-freemarker" implementation 'org.springframework.boot:spring-boot-starter-validation' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 12d38de6a..2a563242c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index 3f230cca8..da157adfe 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,8 @@ +pluginManagement { + repositories { + maven { url 'https://repo.spring.io/milestone' } + gradlePluginPortal() + } +} + rootProject.name = 'halo' diff --git a/src/main/java/run/halo/app/config/HaloMvcConfiguration.java b/src/main/java/run/halo/app/config/HaloMvcConfiguration.java index aee099a83..31be3f40a 100644 --- a/src/main/java/run/halo/app/config/HaloMvcConfiguration.java +++ b/src/main/java/run/halo/app/config/HaloMvcConfiguration.java @@ -6,15 +6,11 @@ import static run.halo.app.utils.HaloUtils.ensureBoth; import static run.halo.app.utils.HaloUtils.ensureSuffix; import com.fasterxml.jackson.databind.ObjectMapper; -import freemarker.core.TemplateClassResolver; -import freemarker.template.TemplateException; -import freemarker.template.TemplateExceptionHandler; import freemarker.template.TemplateModel; import java.io.IOException; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.concurrent.TimeUnit; import javax.servlet.MultipartConfigElement; import javax.servlet.http.HttpServletRequest; @@ -25,6 +21,7 @@ import org.apache.commons.fileupload.FileUploadBase; import org.apache.commons.fileupload.servlet.ServletRequestContext; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties; import org.springframework.boot.autoconfigure.web.servlet.WebMvcRegistrations; @@ -32,6 +29,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.boot.jackson.JsonComponentModule; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.FileUrlResource; import org.springframework.data.domain.PageImpl; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; import org.springframework.data.web.SortHandlerMethodArgumentResolver; @@ -41,6 +39,7 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.lang.NonNull; +import org.springframework.util.StringUtils; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.commons.CommonsMultipartResolver; @@ -49,7 +48,6 @@ import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; import run.halo.app.config.properties.HaloProperties; import run.halo.app.core.PageJacksonSerializer; @@ -85,7 +83,7 @@ public class HaloMvcConfiguration implements WebMvcConfigurer { this.haloProperties = haloProperties; } - @Bean + // @Bean public Map freemarkerLayoutDirectives() { Map freemarkerLayoutDirectives = new HashMap<>(); freemarkerLayoutDirectives.put("extends", new ThemeExtendsDirective()); @@ -95,53 +93,16 @@ public class HaloMvcConfiguration implements WebMvcConfigurer { return freemarkerLayoutDirectives; } - /** - * Configuring freemarker template file path. - * - * @return new FreeMarkerConfigurer - */ - @Bean - FreeMarkerConfigurer freemarkerConfig(HaloProperties haloProperties) - throws IOException, TemplateException { - FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); - configurer - .setTemplateLoaderPaths(FILE_PROTOCOL + haloProperties.getWorkDir() + "templates/", - "classpath:/templates/"); - configurer.setDefaultEncoding("UTF-8"); - - Properties properties = new Properties(); - properties.setProperty("auto_import", - "/common/macro/common_macro.ftl as common,/common/macro/global_macro.ftl as global"); - - configurer.setFreemarkerSettings(properties); - - // Predefine configuration - freemarker.template.Configuration configuration = configurer.createConfiguration(); - - configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER); - - if (haloProperties.isProductionEnv()) { - configuration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); - } - - configuration.setSharedVariables(new HashMap<>() {{ - put("layout", freemarkerLayoutDirectives()); - } - }); - - // Set predefined freemarker configuration - configurer.setConfiguration(configuration); - - return configurer; - } - /** * Configuring multipartResolver for large file upload.. * * @return new multipartResolver */ @Bean(name = "multipartResolver") - MultipartResolver multipartResolver(MultipartProperties multipartProperties) { + @ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", + havingValue = "true", matchIfMissing = true) + MultipartResolver multipartResolver(MultipartProperties multipartProperties) + throws IOException { MultipartConfigElement multipartConfigElement = multipartProperties.createMultipartConfig(); CommonsMultipartResolver resolver = new CommonsMultipartResolver() { @Override @@ -156,10 +117,15 @@ public class HaloMvcConfiguration implements WebMvcConfigurer { resolver.setDefaultEncoding("UTF-8"); resolver.setMaxUploadSize(multipartConfigElement.getMaxRequestSize()); resolver.setMaxUploadSizePerFile(multipartConfigElement.getMaxFileSize()); + var location = multipartProperties.getLocation(); + if (StringUtils.hasText(location)) { + FileUrlResource resource = new FileUrlResource(location); + resolver.setUploadTempDir(resource); + } //lazy multipart parsing, throwing parse exceptions once the application attempts to // obtain multipart files - resolver.setResolveLazily(true); + resolver.setResolveLazily(multipartProperties.isResolveLazily()); return resolver; } diff --git a/src/main/java/run/halo/app/config/properties/HaloProperties.java b/src/main/java/run/halo/app/config/properties/HaloProperties.java index ee3a1be6e..99c93d5a5 100644 --- a/src/main/java/run/halo/app/config/properties/HaloProperties.java +++ b/src/main/java/run/halo/app/config/properties/HaloProperties.java @@ -6,8 +6,6 @@ import static run.halo.app.model.support.HaloConst.USER_HOME; import static run.halo.app.utils.HaloUtils.ensureSuffix; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import run.halo.app.model.enums.Mode; diff --git a/src/main/java/run/halo/app/controller/content/ContentContentController.java b/src/main/java/run/halo/app/controller/content/ContentContentController.java index 01c73b3e9..296069ea7 100644 --- a/src/main/java/run/halo/app/controller/content/ContentContentController.java +++ b/src/main/java/run/halo/app/controller/content/ContentContentController.java @@ -3,6 +3,7 @@ package run.halo.app.controller.content; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Controller; @@ -12,7 +13,8 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import run.halo.app.cache.AbstractStringCacheStore; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import run.halo.app.cache.lock.CacheLock; import run.halo.app.controller.content.model.CategoryModel; import run.halo.app.controller.content.model.JournalModel; @@ -66,8 +68,6 @@ public class ContentContentController { private final SheetService sheetService; - private final AbstractStringCacheStore cacheStore; - private final AuthenticationService authenticationService; private final CategoryService categoryService; @@ -82,7 +82,6 @@ public class ContentContentController { OptionService optionService, PostService postService, SheetService sheetService, - AbstractStringCacheStore cacheStore, AuthenticationService authenticationService, CategoryService categoryService) { this.postModel = postModel; @@ -95,7 +94,6 @@ public class ContentContentController { this.optionService = optionService; this.postService = postService; this.sheetService = sheetService; - this.cacheStore = cacheStore; this.authenticationService = authenticationService; this.categoryService = categoryService; } @@ -126,12 +124,14 @@ public class ContentContentController { Sheet sheet = sheetService.getBySlug(prefix); return sheetModel.content(sheet, token, model); } - throw new NotFoundException("Not Found"); + + throw buildPathNotFoundException(); } @GetMapping("{prefix}/page/{page:\\d+}") public String content(@PathVariable("prefix") String prefix, @PathVariable(value = "page") Integer page, + HttpServletRequest request, Model model) { if (optionService.getArchivesPrefix().equals(prefix)) { return postModel.archives(page, model); @@ -145,7 +145,7 @@ public class ContentContentController { return photoModel.list(page, model); } - throw new NotFoundException("Not Found"); + throw buildPathNotFoundException(); } @GetMapping("{prefix}/{slug}") @@ -186,7 +186,7 @@ public class ContentContentController { return sheetModel.content(sheet, token, model); } - throw new NotFoundException("Not Found"); + throw buildPathNotFoundException(); } @GetMapping("{prefix}/{slug}/page/{page:\\d+}") @@ -202,7 +202,7 @@ public class ContentContentController { return tagModel.listPost(model, slug, page); } - throw new NotFoundException("Not Found"); + throw buildPathNotFoundException(); } @GetMapping("{year:\\d+}/{month:\\d+}/{slug}") @@ -217,7 +217,7 @@ public class ContentContentController { return postModel.content(post, token, model); } - throw new NotFoundException("Not Found"); + throw buildPathNotFoundException(); } @GetMapping("{year:\\d+}/{month:\\d+}/{day:\\d+}/{slug}") @@ -233,7 +233,7 @@ public class ContentContentController { return postModel.content(post, token, model); } - throw new NotFoundException("Not Found"); + throw buildPathNotFoundException(); } @PostMapping(value = "content/{type}/{slug:.*}/authentication") @@ -254,6 +254,17 @@ public class ContentContentController { return "redirect:" + redirectUrl; } + private NotFoundException buildPathNotFoundException() { + var requestAttributes = RequestContextHolder.currentRequestAttributes(); + + var requestUri = ""; + if (requestAttributes instanceof ServletRequestAttributes) { + requestUri = + ((ServletRequestAttributes) requestAttributes).getRequest().getRequestURI(); + } + return new NotFoundException("无法定位到该路径:" + requestUri); + } + private String doAuthenticationPost( String slug, String password) throws UnsupportedEncodingException { Post post = postService.getBy(PostStatus.INTIMATE, slug); diff --git a/src/main/java/run/halo/app/controller/core/CommonController.java b/src/main/java/run/halo/app/controller/core/CommonController.java deleted file mode 100644 index c018d1fbd..000000000 --- a/src/main/java/run/halo/app/controller/core/CommonController.java +++ /dev/null @@ -1,234 +0,0 @@ -package run.halo.app.controller.core; - -import static run.halo.app.model.support.HaloConst.DEFAULT_ERROR_PATH; - -import java.util.Collections; -import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController; -import org.springframework.boot.web.error.ErrorAttributeOptions; -import org.springframework.boot.web.servlet.error.ErrorAttributes; -import org.springframework.http.HttpStatus; -import org.springframework.lang.NonNull; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.util.Assert; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.util.NestedServletException; -import run.halo.app.exception.AbstractHaloException; -import run.halo.app.exception.NotFoundException; -import run.halo.app.service.OptionService; -import run.halo.app.service.ThemeService; -import run.halo.app.utils.FilenameUtils; - -/** - * Error page Controller - * - * @author ryanwang - * @date 2017-12-26 - */ -@Slf4j -@Controller -@RequestMapping("${server.error.path:${error.path:/error}}") -public class CommonController extends AbstractErrorController { - - private static final String NOT_FOUND_TEMPLATE = "404.ftl"; - - private static final String INTERNAL_ERROR_TEMPLATE = "500.ftl"; - - private static final String ERROR_TEMPLATE = "error.ftl"; - - private static final String COULD_NOT_RESOLVE_VIEW_WITH_NAME_PREFIX = - "Could not resolve view with name '"; - - private final ThemeService themeService; - - private final ErrorProperties errorProperties; - - private final OptionService optionService; - - public CommonController(ThemeService themeService, - ErrorAttributes errorAttributes, - ServerProperties serverProperties, - OptionService optionService) { - super(errorAttributes); - this.themeService = themeService; - this.errorProperties = serverProperties.getError(); - this.optionService = optionService; - } - - /** - * Handle error - * - * @param request request - * @return String - */ - @GetMapping - public String handleError(HttpServletRequest request, HttpServletResponse response, - Model model) { - handleCustomException(request); - - ErrorAttributeOptions options = getErrorAttributeOptions(request); - - Map errorDetail = - Collections.unmodifiableMap(getErrorAttributes(request, options)); - model.addAttribute("error", errorDetail); - model.addAttribute("meta_keywords", optionService.getSeoKeywords()); - model.addAttribute("meta_description", optionService.getSeoDescription()); - log.debug("Error detail: [{}]", errorDetail); - - HttpStatus status = getStatus(request); - - response.setStatus(status.value()); - if (status.equals(HttpStatus.INTERNAL_SERVER_ERROR)) { - return contentInternalError(); - } else if (status.equals(HttpStatus.NOT_FOUND)) { - return contentNotFound(); - } else { - return defaultErrorHandler(); - } - } - - /** - * Render 404 error page - * - * @return String - */ - @GetMapping(value = "/404") - public String contentNotFound() { - if (themeService.templateExists(ERROR_TEMPLATE)) { - return getActualTemplatePath(ERROR_TEMPLATE); - } - - if (themeService.templateExists(NOT_FOUND_TEMPLATE)) { - return getActualTemplatePath(NOT_FOUND_TEMPLATE); - } - - return defaultErrorHandler(); - } - - /** - * Render 500 error page - * - * @return template path: - */ - @GetMapping(value = "/500") - public String contentInternalError() { - if (themeService.templateExists(ERROR_TEMPLATE)) { - return getActualTemplatePath(ERROR_TEMPLATE); - } - - if (themeService.templateExists(INTERNAL_ERROR_TEMPLATE)) { - return getActualTemplatePath(INTERNAL_ERROR_TEMPLATE); - } - - return defaultErrorHandler(); - } - - private String defaultErrorHandler() { - return DEFAULT_ERROR_PATH; - } - - private String getActualTemplatePath(@NonNull String template) { - Assert.hasText(template, "FTL template must not be blank"); - - StringBuilder path = new StringBuilder(); - path.append("themes/") - .append(themeService.getActivatedTheme().getFolderName()) - .append('/') - .append(FilenameUtils.getBasename(template)); - - return path.toString(); - } - - /** - * Handles custom exception, like HaloException. - * - * @param request http servlet request must not be null - */ - private void handleCustomException(@NonNull HttpServletRequest request) { - Assert.notNull(request, "Http servlet request must not be null"); - - Object throwableObject = request.getAttribute("javax.servlet.error.exception"); - if (throwableObject == null) { - return; - } - - Throwable throwable = (Throwable) throwableObject; - - if (throwable instanceof NestedServletException) { - log.error("Captured an exception: [{}]", throwable.getMessage()); - Throwable rootCause = ((NestedServletException) throwable).getRootCause(); - if (rootCause instanceof AbstractHaloException) { - if (!(rootCause instanceof NotFoundException)) { - log.error("Caused by", rootCause); - } - AbstractHaloException haloException = (AbstractHaloException) rootCause; - request.setAttribute("javax.servlet.error.status_code", - haloException.getStatus().value()); - request.setAttribute("javax.servlet.error.exception", rootCause); - request.setAttribute("javax.servlet.error.message", haloException.getMessage()); - } - } else if (StringUtils.startsWithIgnoreCase(throwable.getMessage(), - COULD_NOT_RESOLVE_VIEW_WITH_NAME_PREFIX)) { - log.debug("Captured an exception", throwable); - request.setAttribute("javax.servlet.error.status_code", HttpStatus.NOT_FOUND.value()); - - NotFoundException viewNotFound = new NotFoundException("该路径没有对应的模板"); - request.setAttribute("javax.servlet.error.exception", viewNotFound); - request.setAttribute("javax.servlet.error.message", viewNotFound.getMessage()); - } - - } - - /** - * Returns the path of the error page. - * - * @return the error path - */ - @Override - public String getErrorPath() { - return this.errorProperties.getPath(); - } - - /** - * Determine if the stacktrace attribute should be included. - * - * @param request the source request - * @return if the stacktrace attribute should be included - */ - private boolean isIncludeStackTrace(HttpServletRequest request) { - ErrorProperties.IncludeStacktrace include = errorProperties.getIncludeStacktrace(); - if (include == ErrorProperties.IncludeStacktrace.ALWAYS) { - return true; - } - if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) { - return getTraceParameter(request); - } - return false; - } - - /** - * Get the ErrorAttributeOptions . - * - * @param request the source request - * @return {@link ErrorAttributeOptions} - */ - private ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request) { - ErrorProperties.IncludeStacktrace include = errorProperties.getIncludeStacktrace(); - if (include == ErrorProperties.IncludeStacktrace.ALWAYS) { - return ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE); - } - if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM - && getTraceParameter(request)) { - return ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE); - } - return ErrorAttributeOptions.defaults(); - } -} diff --git a/src/main/java/run/halo/app/controller/error/DefaultErrorController.java b/src/main/java/run/halo/app/controller/error/DefaultErrorController.java new file mode 100644 index 000000000..748933e37 --- /dev/null +++ b/src/main/java/run/halo/app/controller/error/DefaultErrorController.java @@ -0,0 +1,61 @@ +package run.halo.app.controller.error; + +import java.util.List; +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver; +import org.springframework.boot.web.servlet.error.ErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.util.NestedServletException; +import run.halo.app.exception.AbstractHaloException; + +/** + * Default error controller. + * + * @author johnniang + */ +@Component +@Slf4j +public class DefaultErrorController extends BasicErrorController { + + public DefaultErrorController( + ErrorAttributes errorAttributes, + ServerProperties serverProperties, + List errorViewResolvers) { + super(errorAttributes, serverProperties.getError(), errorViewResolvers); + } + + @Override + protected HttpStatus getStatus(HttpServletRequest request) { + var status = super.getStatus(request); + // deduce status + var exception = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); + if (exception instanceof NestedServletException) { + var nse = (NestedServletException) exception; + if (nse.getCause() instanceof AbstractHaloException) { + status = resolveHaloException((AbstractHaloException) nse.getCause(), request); + } + } else if (exception instanceof AbstractHaloException) { + status = resolveHaloException((AbstractHaloException) exception, request); + } + return status; + } + + private HttpStatus resolveHaloException(AbstractHaloException haloException, + HttpServletRequest request) { + HttpStatus status = haloException.getStatus(); + if (log.isDebugEnabled()) { + log.error("Halo exception occurred.", haloException); + } + // reset status + request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, status.value()); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION, haloException); + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, haloException.getMessage()); + request.setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, haloException.getClass()); + return status; + } +} diff --git a/src/main/java/run/halo/app/controller/error/DefaultErrorViewResolver.java b/src/main/java/run/halo/app/controller/error/DefaultErrorViewResolver.java new file mode 100644 index 000000000..d78dc6980 --- /dev/null +++ b/src/main/java/run/halo/app/controller/error/DefaultErrorViewResolver.java @@ -0,0 +1,79 @@ +package run.halo.app.controller.error; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; +import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatus.Series; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.ModelAndView; +import run.halo.app.service.ThemeService; + +@Component +public class DefaultErrorViewResolver implements ErrorViewResolver { + + private static final Map SERIES_VIEWS; + + static { + EnumMap views = new EnumMap<>(Series.class); + views.put(Series.CLIENT_ERROR, "4xx"); + views.put(Series.SERVER_ERROR, "5xx"); + SERIES_VIEWS = Collections.unmodifiableMap(views); + } + + private final ThemeService themeService; + + private final TemplateAvailabilityProviders templateAvailabilityProviders; + + private final ApplicationContext applicationContext; + + public DefaultErrorViewResolver(ThemeService themeService, + ApplicationContext applicationContext) { + this.themeService = themeService; + this.applicationContext = applicationContext; + this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext); + } + + @Override + public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, + Map model) { + // for compatibility + var errorModel = new HashMap<>(model); + + // resolve with status code. eg: 400.ftl + var modelAndView = resolve(String.valueOf(status.value()), errorModel); + + // resolve with status series. eg: 4xx.ftl + if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) { + modelAndView = resolve(SERIES_VIEWS.get(status.series()), errorModel); + } + + // resolve error template. eg: error.ftl + if (modelAndView == null) { + modelAndView = resolve("error", errorModel); + } + + if (modelAndView == null) { + // resolve common error template + modelAndView = new ModelAndView("common/error/error", errorModel); + } + + return modelAndView; + } + + private ModelAndView resolve(String viewName, Map model) { + var errorViewName = this.themeService.render(viewName); + var provider = + this.templateAvailabilityProviders.getProvider(errorViewName, this.applicationContext); + if (provider != null) { + return new ModelAndView(errorViewName, model); + } + return null; + } + +} diff --git a/src/main/java/run/halo/app/core/CommonResultControllerAdvice.java b/src/main/java/run/halo/app/core/CommonResultControllerAdvice.java index 4c1b8408a..8393089f3 100644 --- a/src/main/java/run/halo/app/core/CommonResultControllerAdvice.java +++ b/src/main/java/run/halo/app/core/CommonResultControllerAdvice.java @@ -9,6 +9,7 @@ import org.springframework.http.converter.json.AbstractJackson2HttpMessageConver import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -61,16 +62,28 @@ public class CommonResultControllerAdvice implements ResponseBodyAdvice Object returnBody = bodyContainer.getValue(); if (returnBody instanceof BaseResponse) { - // If the return body is instance of BaseResponse + // If the return body is instance of BaseResponse, then just do nothing BaseResponse baseResponse = (BaseResponse) returnBody; - response.setStatusCode(HttpStatus.resolve(baseResponse.getStatus())); + HttpStatus status = HttpStatus.resolve(baseResponse.getStatus()); + if (status == null) { + status = HttpStatus.INTERNAL_SERVER_ERROR; + } + response.setStatusCode(status); return; } - // Wrap the return body - BaseResponse baseResponse = BaseResponse.ok(returnBody); + // get status + var status = HttpStatus.OK; + if (response instanceof ServletServerHttpResponse) { + var servletResponse = + ((ServletServerHttpResponse) response).getServletResponse(); + status = HttpStatus.resolve(servletResponse.getStatus()); + if (status == null) { + status = HttpStatus.INTERNAL_SERVER_ERROR; + } + } + var baseResponse = new BaseResponse<>(status.value(), status.getReasonPhrase(), returnBody); bodyContainer.setValue(baseResponse); - response.setStatusCode(HttpStatus.valueOf(baseResponse.getStatus())); } } diff --git a/src/main/java/run/halo/app/exception/ThemeNotFoundException.java b/src/main/java/run/halo/app/exception/ThemeNotFoundException.java new file mode 100644 index 000000000..217305030 --- /dev/null +++ b/src/main/java/run/halo/app/exception/ThemeNotFoundException.java @@ -0,0 +1,18 @@ +package run.halo.app.exception; + +/** + * Theme not found exception. + * + * @author johnniang + * @date 2020-03-01 + */ +public class ThemeNotFoundException extends BadRequestException { + + public ThemeNotFoundException(String message) { + super(message); + } + + public ThemeNotFoundException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/run/halo/app/listener/StartedListener.java b/src/main/java/run/halo/app/listener/StartedListener.java index 2fb7ff317..9be598322 100644 --- a/src/main/java/run/halo/app/listener/StartedListener.java +++ b/src/main/java/run/halo/app/listener/StartedListener.java @@ -1,5 +1,6 @@ package run.halo.app.listener; +import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.nio.file.FileSystem; @@ -135,7 +136,7 @@ public class StartedListener implements ApplicationListener freemarkerLayoutDirectives() { + Map freemarkerLayoutDirectives = new HashMap<>(); + freemarkerLayoutDirectives.put("extends", new ThemeExtendsDirective()); + freemarkerLayoutDirectives.put("block", new BlockDirective()); + freemarkerLayoutDirectives.put("put", new PutDirective()); + return freemarkerLayoutDirectives; + } + + private void initFreemarkerConfig() throws TemplateModelException { + configuration.setSharedVariable("layout", freemarkerLayoutDirectives()); } @EventListener @@ -72,15 +92,14 @@ public class FreemarkerConfigAwareListener { } @EventListener - public void onThemeActivatedEvent(ThemeActivatedEvent themeActivatedEvent) - throws TemplateModelException { + public void onThemeActivatedEvent(ThemeActivatedEvent themeActivatedEvent) { log.debug("Received theme activated event"); loadThemeConfig(); } @EventListener - public void onThemeUpdatedEvent(ThemeUpdatedEvent event) throws TemplateModelException { + public void onThemeUpdatedEvent(ThemeUpdatedEvent event) { log.debug("Received theme updated event"); loadThemeConfig(); diff --git a/src/main/java/run/halo/app/model/support/HaloConst.java b/src/main/java/run/halo/app/model/support/HaloConst.java index 097d5080f..1ce32d483 100644 --- a/src/main/java/run/halo/app/model/support/HaloConst.java +++ b/src/main/java/run/halo/app/model/support/HaloConst.java @@ -5,9 +5,7 @@ import java.util.Optional; import org.springframework.http.HttpHeaders; /** - *
- *     公共常量
- * 
+ * Halo constants. * * @author ryanwang * @date 2017/12/29 @@ -17,12 +15,12 @@ public class HaloConst { /** * User home directory. */ - public static final String USER_HOME = System.getProperties().getProperty("user.home"); + public static final String USER_HOME = System.getProperty("user.home"); /** * Temporary directory. */ - public static final String TEMP_DIR = "/tmp/run.halo.app"; + public static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); public static final String PROTOCOL_HTTPS = "https://"; @@ -65,7 +63,7 @@ public class HaloConst { */ public static final String POST_PASSWORD_TEMPLATE = "post_password"; /** - * Suffix of freemarker template file + * Suffix of freemarker template file. */ public static final String SUFFIX_FTL = ".ftl"; /** diff --git a/src/main/java/run/halo/app/repository/ThemeRepositoryImpl.java b/src/main/java/run/halo/app/repository/ThemeRepositoryImpl.java index 778b043d3..4c919e71c 100644 --- a/src/main/java/run/halo/app/repository/ThemeRepositoryImpl.java +++ b/src/main/java/run/halo/app/repository/ThemeRepositoryImpl.java @@ -13,6 +13,7 @@ import java.nio.file.Paths; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.context.ApplicationEventPublisher; @@ -25,6 +26,7 @@ import run.halo.app.event.options.OptionUpdatedEvent; import run.halo.app.exception.AlreadyExistsException; import run.halo.app.exception.NotFoundException; import run.halo.app.exception.ServiceException; +import run.halo.app.exception.ThemeNotFoundException; import run.halo.app.exception.ThemeNotSupportException; import run.halo.app.handler.theme.config.support.ThemeProperty; import run.halo.app.model.entity.Option; @@ -67,16 +69,30 @@ public class ThemeRepositoryImpl public ThemeProperty getActivatedThemeProperty() { ThemeProperty themeProperty = this.currentTheme; if (themeProperty == null) { + AtomicBoolean fallbackTheme = new AtomicBoolean(false); synchronized (this) { if (this.currentTheme == null) { // get current theme id String currentThemeId = this.optionRepository.findByKey(THEME.getValue()) .map(Option::getValue) .orElse(DEFAULT_THEME_ID); + // fetch current theme - this.currentTheme = this.getThemeByThemeId(currentThemeId); + this.currentTheme = + this.fetchThemeByThemeId(currentThemeId).orElseGet(() -> { + if (!StringUtils.equalsIgnoreCase(currentThemeId, DEFAULT_THEME_ID)) { + fallbackTheme.set(true); + return this.getThemeByThemeId(DEFAULT_THEME_ID); + } + throw new ThemeNotFoundException( + "Default theme: " + DEFAULT_THEME_ID + " was not found!"); + }); } } + if (fallbackTheme.get()) { + // need set default theme as fallback theme + setActivatedTheme(DEFAULT_THEME_ID); + } } return this.currentTheme; } @@ -188,16 +204,22 @@ public class ThemeRepositoryImpl @Override public void onApplicationEvent(OptionUpdatedEvent event) { synchronized (this) { + // reset current theme with null this.currentTheme = null; } } @NonNull protected ThemeProperty getThemeByThemeId(String themeId) { + return fetchThemeByThemeId(themeId).orElseThrow( + () -> new ThemeNotFoundException("Failed to find theme with id: " + themeId)); + } + + @NonNull + protected Optional fetchThemeByThemeId(String themeId) { return ThemePropertyScanner.INSTANCE.scan(getThemeRootPath(), null) .stream() .filter(property -> Objects.equals(themeId, property.getId())) - .findFirst() - .orElseThrow(); + .findFirst(); } } diff --git a/src/main/java/run/halo/app/service/impl/OptionServiceImpl.java b/src/main/java/run/halo/app/service/impl/OptionServiceImpl.java index ea6ccc2a6..0058a93fb 100644 --- a/src/main/java/run/halo/app/service/impl/OptionServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/OptionServiceImpl.java @@ -72,15 +72,12 @@ public class OptionServiceImpl extends AbstractCrudService private final AbstractStringCacheStore cacheStore; private final Map propertyEnumMap; private final ApplicationEventPublisher eventPublisher; - private final HaloProperties haloProperties; - public OptionServiceImpl(HaloProperties haloProperties, - OptionRepository optionRepository, + public OptionServiceImpl(OptionRepository optionRepository, ApplicationContext applicationContext, AbstractStringCacheStore cacheStore, ApplicationEventPublisher eventPublisher) { super(optionRepository); - this.haloProperties = haloProperties; this.optionRepository = optionRepository; this.applicationContext = applicationContext; this.cacheStore = cacheStore; diff --git a/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java b/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java index 5c50772fa..b6144d687 100644 --- a/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/ThemeServiceImpl.java @@ -1,6 +1,5 @@ package run.halo.app.service.impl; -import static run.halo.app.model.support.HaloConst.DEFAULT_ERROR_PATH; import static run.halo.app.utils.FileUtils.copyFolder; import static run.halo.app.utils.FileUtils.deleteFolderQuietly; import static run.halo.app.utils.VersionUtil.compareVersion; @@ -314,18 +313,14 @@ public class ThemeServiceImpl implements ThemeService { @Override public String render(String pageName) { - return fetchActivatedTheme() - .map(themeProperty -> - String.format(RENDER_TEMPLATE, themeProperty.getFolderName(), pageName)) - .orElse(DEFAULT_ERROR_PATH); + var folderName = getActivatedTheme().getFolderName(); + return "themes/" + folderName + "/" + pageName; } @Override public String renderWithSuffix(String pageName) { - // Get activated theme - ThemeProperty activatedTheme = getActivatedTheme(); - // Build render url - return String.format(RENDER_TEMPLATE_SUFFIX, activatedTheme.getFolderName(), pageName); + var folderName = getActivatedTheme().getFolderName(); + return "themes/" + folderName + "/" + pageName + ".ftl"; } @Override @@ -337,13 +332,13 @@ public class ThemeServiceImpl implements ThemeService { @Override @NonNull public ThemeProperty getActivatedTheme() { - return themeRepository.getActivatedThemeProperty(); + return fetchActivatedTheme().orElseThrow(); } @Override @NonNull public Optional fetchActivatedTheme() { - return fetchThemePropertyBy(getActivatedThemeId()); + return Optional.of(themeRepository.getActivatedThemeProperty()); } @Override diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index f57b0f5d1..ac296abde 100755 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,6 +1,8 @@ server: port: 8090 forward-headers-strategy: native + error: + include-message: always compression: enabled: false spring: @@ -40,7 +42,14 @@ spring: multipart: max-file-size: 10240MB max-request-size: 10240MB - location: /tmp/run.halo.app + resolve-lazily: true + freemarker: + suffix: .ftl + settings: + auto_import: /common/macro/global_macro.ftl as global + template-loader-path: + - file:///${halo.work-dir}templates/ + - classpath:/templates/ management: endpoints: web: @@ -60,4 +69,5 @@ springfox: halo: download-timeout: 5m - cache: memory \ No newline at end of file + cache: memory + work-dir: ${user.home}/.halo/ \ No newline at end of file diff --git a/src/main/resources/templates/common/error/404.ftl b/src/main/resources/templates/common/error/404.ftl deleted file mode 100644 index 44d986c4b..000000000 --- a/src/main/resources/templates/common/error/404.ftl +++ /dev/null @@ -1 +0,0 @@ -404 Not Found diff --git a/src/main/resources/templates/common/error/500.ftl b/src/main/resources/templates/common/error/500.ftl deleted file mode 100644 index d2bb962a3..000000000 --- a/src/main/resources/templates/common/error/500.ftl +++ /dev/null @@ -1 +0,0 @@ -500 Internal Error diff --git a/src/main/resources/templates/common/error/error.ftl b/src/main/resources/templates/common/error/error.ftl index aa0fc4e00..1f0bf2a63 100644 --- a/src/main/resources/templates/common/error/error.ftl +++ b/src/main/resources/templates/common/error/error.ftl @@ -5,7 +5,7 @@ - ${(error.status)!500} | ${(error.error)!'未知错误'} + ${(status)!500} | ${(error)!'未知错误'}