diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 0e1231212..4b2db5cfa 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,6 +1,8 @@ -name: Java CI +name: Halo CI -on: [push] +on: + pull_request: + push: jobs: build: @@ -8,10 +10,22 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 - - name: Set up JDK 1.8 - uses: actions/setup-java@v1 - with: - java-version: 1.8 - - name: Build with Gradle - run: ./gradlew build + - uses: actions/checkout@v1 + with: + submodules: true + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Cache Gradle + id: cache-gradle + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Test with Gradle + run: ./gradlew test + - name: Build with Gradle + run: ./gradlew build -x test diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..9d599f224 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "default-theme"] + path = src/main/resources/templates/themes/anatole + url = https://github.com/halo-dev/halo-theme-anatole diff --git a/build.gradle b/build.gradle index e2af3f20d..6ac183adb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'org.springframework.boot' version '2.2.1.RELEASE' + id 'org.springframework.boot' version '2.2.2.RELEASE' id "io.freefair.lombok" version "3.6.6" // id 'war' id 'java' @@ -59,6 +59,10 @@ ext { image4jVersion = '0.7zensight1' flywayVersion = '6.1.0' h2Version = '1.4.196' + levelDbVersion = '0.12' + jsonVersion = '20190722' + fastJsonVersion = '1.2.56' + templateInheritance = "0.4.RELEASE" } dependencies { @@ -67,6 +71,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-undertow' implementation 'org.springframework.boot:spring-boot-starter-freemarker' + implementation "kr.pe.kwonnam.freemarker:freemarker-template-inheritance:$templateInheritance" implementation "io.github.biezhi:oh-my-email:$ohMyEmailVersion" implementation "cn.hutool:hutool-core:$hutoolVersion" @@ -103,6 +108,10 @@ dependencies { implementation "net.sf.image4j:image4j:$image4jVersion" implementation "org.flywaydb:flyway-core:$flywayVersion" + implementation "org.json:json:$jsonVersion" + implementation "com.alibaba:fastjson:$fastJsonVersion" + + implementation "org.iq80.leveldb:leveldb:$levelDbVersion" runtimeOnly "com.h2database:h2:$h2Version" runtimeOnly 'mysql:mysql-connector-java' diff --git a/haloCodeStyle.xml b/haloCodeStyle.xml new file mode 100644 index 000000000..8756f6089 --- /dev/null +++ b/haloCodeStyle.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/run/halo/app/Application.java b/src/main/java/run/halo/app/Application.java index 4735c15b8..6b7852598 100755 --- a/src/main/java/run/halo/app/Application.java +++ b/src/main/java/run/halo/app/Application.java @@ -33,6 +33,7 @@ public class Application extends SpringBootServletInitializer { // Run application context = SpringApplication.run(Application.class, args); + } /** diff --git a/src/main/java/run/halo/app/cache/LevelCacheStore.java b/src/main/java/run/halo/app/cache/LevelCacheStore.java new file mode 100644 index 000000000..954b90d8f --- /dev/null +++ b/src/main/java/run/halo/app/cache/LevelCacheStore.java @@ -0,0 +1,163 @@ +package run.halo.app.cache; + +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.extern.slf4j.Slf4j; +import org.iq80.leveldb.*; +import org.iq80.leveldb.impl.Iq80DBFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import run.halo.app.config.properties.HaloProperties; +import run.halo.app.utils.JsonUtils; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.*; + +/** + * level-db cache store + * Create by Pencilso on 2020/1/9 7:20 下午 + */ +@Slf4j +public class LevelCacheStore extends StringCacheStore { + /** + * Cleaner schedule period. (ms) + */ + private final static long PERIOD = 60 * 1000; + + private static DB leveldb; + + + private Timer timer; + + @Autowired + private HaloProperties haloProperties; + + @PostConstruct + public void init() { + if (leveldb != null) return; + try { + //work path + File folder = new File(haloProperties.getWorkDir() + ".leveldb"); + DBFactory factory = new Iq80DBFactory(); + Options options = new Options(); + options.createIfMissing(true); + //open leveldb store folder + leveldb = factory.open(folder, options); + timer = new Timer(); + timer.scheduleAtFixedRate(new CacheExpiryCleaner(), 0, PERIOD); + } catch (Exception ex) { + log.error("init leveldb error ", ex); + } + } + + /** + * 销毁 + */ + @PreDestroy + public void preDestroy() { + try { + leveldb.close(); + timer.cancel(); + } catch (IOException e) { + log.error("close leveldb error ", e); + } + } + + @Override + Optional> getInternal(String key) { + Assert.hasText(key, "Cache key must not be blank"); + byte[] bytes = leveldb.get(stringToBytes(key)); + if (bytes != null) { + String valueJson = bytesToString(bytes); + return StringUtils.isEmpty(valueJson) ? Optional.empty() : jsonToCacheWrapper(valueJson); + } + return Optional.empty(); + } + + @Override + void putInternal(String key, CacheWrapper cacheWrapper) { + putInternalIfAbsent(key, cacheWrapper); + } + + @Override + Boolean putInternalIfAbsent(String key, CacheWrapper cacheWrapper) { + Assert.hasText(key, "Cache key must not be blank"); + Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); + try { + leveldb.put( + stringToBytes(key), + stringToBytes(JsonUtils.objectToJson(cacheWrapper)) + ); + return true; + } catch (JsonProcessingException e) { + log.warn("Put cache fail json2object key: [{}] value:[{}]", key, cacheWrapper); + } + log.debug("Cache key: [{}], original cache wrapper: [{}]", key, cacheWrapper); + return false; + } + + @Override + public void delete(String key) { + leveldb.delete(stringToBytes(key)); + log.debug("cache remove key: [{}]", key); + } + + + private byte[] stringToBytes(String str) { + return str.getBytes(Charset.defaultCharset()); + } + + private String bytesToString(byte[] bytes) { + return new String(bytes, Charset.defaultCharset()); + } + + private Optional> jsonToCacheWrapper(String json) { + Assert.hasText(json, "json value must not be null"); + CacheWrapper cacheWrapper = null; + try { + cacheWrapper = JsonUtils.jsonToObject(json, CacheWrapper.class); + } catch (IOException e) { + e.printStackTrace(); + log.debug("erro json to wrapper value bytes: [{}]", json, e); + } + return Optional.ofNullable(cacheWrapper); + } + + private class CacheExpiryCleaner extends TimerTask { + + @Override + public void run() { + //batch + WriteBatch writeBatch = leveldb.createWriteBatch(); + + DBIterator iterator = leveldb.iterator(); + long currentTimeMillis = System.currentTimeMillis(); + while (iterator.hasNext()) { + Map.Entry next = iterator.next(); + if (next.getKey() == null || next.getValue() == null) { + continue; + } + + String valueJson = bytesToString(next.getValue()); + Optional> stringCacheWrapper = StringUtils.isEmpty(valueJson) ? Optional.empty() : jsonToCacheWrapper(valueJson); + if (stringCacheWrapper.isPresent()) { + //get expireat time + long expireAtTime = stringCacheWrapper.map(CacheWrapper::getExpireAt) + .map(Date::getTime) + .orElse(0L); + //if expire + if (expireAtTime != 0 && currentTimeMillis > expireAtTime) { + writeBatch.delete(next.getKey()); + log.debug("deleted the cache: [{}] for expiration", bytesToString(next.getKey())); + } + } + } + leveldb.write(writeBatch); + } + } +} + diff --git a/src/main/java/run/halo/app/config/HaloConfiguration.java b/src/main/java/run/halo/app/config/HaloConfiguration.java index 925a816d8..eb67f0bf2 100644 --- a/src/main/java/run/halo/app/config/HaloConfiguration.java +++ b/src/main/java/run/halo/app/config/HaloConfiguration.java @@ -14,6 +14,7 @@ import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.web.client.RestTemplate; import run.halo.app.cache.InMemoryCacheStore; +import run.halo.app.cache.LevelCacheStore; import run.halo.app.cache.StringCacheStore; import run.halo.app.config.properties.HaloProperties; import run.halo.app.filter.CorsFilter; @@ -63,7 +64,22 @@ public class HaloConfiguration { @Bean @ConditionalOnMissingBean public StringCacheStore stringCacheStore() { - return new InMemoryCacheStore(); + StringCacheStore stringCacheStore; + switch (haloProperties.getCache()) { + case "level": + stringCacheStore = new LevelCacheStore(); + break; + + case "memory": + default: + //memory or default + stringCacheStore = new InMemoryCacheStore(); + break; + + } + log.info("halo cache store load impl : [{}]", stringCacheStore.getClass()); + return stringCacheStore; + } /** diff --git a/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java b/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java index f9b59fd0c..e988d5e11 100644 --- a/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java +++ b/src/main/java/run/halo/app/config/WebMvcAutoConfiguration.java @@ -4,6 +4,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import freemarker.core.TemplateClassResolver; import freemarker.template.TemplateException; import freemarker.template.TemplateExceptionHandler; +import freemarker.template.TemplateModel; +import kr.pe.kwonnam.freemarker.inheritance.BlockDirective; +import kr.pe.kwonnam.freemarker.inheritance.ExtendsDirective; +import kr.pe.kwonnam.freemarker.inheritance.PutDirective; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.jackson.JsonComponentModule; import org.springframework.context.annotation.Bean; @@ -11,15 +15,20 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import org.springframework.data.domain.PageImpl; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.data.web.SortHandlerMethodArgumentResolver; import org.springframework.format.FormatterRegistry; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.HandlerMethodArgumentResolver; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; +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; @@ -28,32 +37,39 @@ import run.halo.app.factory.StringToEnumConverterFactory; import run.halo.app.model.support.HaloConst; import run.halo.app.security.resolver.AuthenticationArgumentResolver; +import javax.servlet.http.HttpServletRequest; import java.io.IOException; -import java.util.List; -import java.util.Properties; +import java.util.*; import static run.halo.app.model.support.HaloConst.FILE_SEPARATOR; import static run.halo.app.model.support.HaloConst.HALO_ADMIN_RELATIVE_PATH; import static run.halo.app.utils.HaloUtils.*; /** - * Mvc configuration. + * Spring mvc configuration. * * @author ryanwang * @date 2018-01-02 */ @Slf4j @Configuration -@EnableWebMvc @ComponentScan(basePackages = "run.halo.app.controller") @PropertySource(value = "classpath:application.yaml", ignoreResourceNotFound = true, encoding = "UTF-8") -public class WebMvcAutoConfiguration implements WebMvcConfigurer { +public class WebMvcAutoConfiguration extends WebMvcConfigurationSupport { private static final String FILE_PROTOCOL = "file:///"; + private final PageableHandlerMethodArgumentResolver pageableResolver; + + private final SortHandlerMethodArgumentResolver sortResolver; + private final HaloProperties haloProperties; - public WebMvcAutoConfiguration(HaloProperties haloProperties) { + public WebMvcAutoConfiguration(PageableHandlerMethodArgumentResolver pageableResolver, + SortHandlerMethodArgumentResolver sortResolver, + HaloProperties haloProperties) { + this.pageableResolver = pageableResolver; + this.sortResolver = sortResolver; this.haloProperties = haloProperties; } @@ -74,6 +90,8 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer { @Override public void addArgumentResolvers(List resolvers) { resolvers.add(new AuthenticationArgumentResolver()); + resolvers.add(pageableResolver); + resolvers.add(sortResolver); } /** @@ -84,13 +102,17 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String workDir = FILE_PROTOCOL + ensureSuffix(haloProperties.getWorkDir(), FILE_SEPARATOR); - String backupDir = FILE_PROTOCOL + ensureSuffix(haloProperties.getBackupDir(), FILE_SEPARATOR); + + // register /** resource handler. registry.addResourceHandler("/**") - .addResourceLocations(workDir + "templates/themes/") .addResourceLocations(workDir + "templates/admin/") .addResourceLocations("classpath:/admin/") .addResourceLocations(workDir + "static/"); + // register /themes/** resource handler. + registry.addResourceHandler("/themes/**") + .addResourceLocations(workDir + "templates/themes/"); + String uploadUrlPattern = ensureBoth(haloProperties.getUploadUrlPrefix(), URL_SEPARATOR) + "**"; String adminPathPattern = ensureSuffix(haloProperties.getAdminPath(), URL_SEPARATOR) + "**"; @@ -109,11 +131,22 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer { } } + @Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(new StringToEnumConverterFactory()); } + @Bean + public Map freemarkerLayoutDirectives() { + Map freemarkerLayoutDirectives = new HashMap<>(5); + freemarkerLayoutDirectives.put("extends", new ExtendsDirective()); + freemarkerLayoutDirectives.put("block", new BlockDirective()); + freemarkerLayoutDirectives.put("put", new PutDirective()); + + return freemarkerLayoutDirectives; + } + /** * Configuring freemarker template file path. * @@ -142,6 +175,13 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer { // Set predefined freemarker configuration configurer.setConfiguration(configuration); + // Set layout variable + Map freemarkerVariables = new HashMap<>(3); + + freemarkerVariables.put("layout", freemarkerLayoutDirectives()); + + configurer.setFreemarkerVariables(freemarkerVariables); + return configurer; } @@ -162,4 +202,54 @@ public class WebMvcAutoConfiguration implements WebMvcConfigurer { resolver.setContentType("text/html; charset=UTF-8"); registry.viewResolver(resolver); } + + @Override + protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { + return new HaloRequestMappingHandlerMapping(haloProperties); + } + + private static class HaloRequestMappingHandlerMapping extends RequestMappingHandlerMapping { + + private final Set blackPatterns = new HashSet<>(16); + + private final PathMatcher pathMatcher; + + private final HaloProperties haloProperties; + + public HaloRequestMappingHandlerMapping(HaloProperties haloProperties) { + this.haloProperties = haloProperties; + this.initBlackPatterns(); + pathMatcher = new AntPathMatcher(); + } + + @Override + protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { + log.debug("Looking path: [{}]", lookupPath); + for (String blackPattern : blackPatterns) { + if (this.pathMatcher.match(blackPattern, lookupPath)) { + log.info("Skipped path [{}] with pattern: [{}]", lookupPath, blackPattern); + return null; + } + } + return super.lookupHandlerMethod(lookupPath, request); + } + + private void initBlackPatterns() { + String uploadUrlPattern = ensureBoth(haloProperties.getUploadUrlPrefix(), URL_SEPARATOR) + "**"; + String adminPathPattern = ensureBoth(haloProperties.getAdminPath(), URL_SEPARATOR) + "**"; + + + blackPatterns.add("/themes/**"); + blackPatterns.add("/js/**"); + blackPatterns.add("/images/**"); + blackPatterns.add("/fonts/**"); + blackPatterns.add("/css/**"); + blackPatterns.add("/assets/**"); + blackPatterns.add("/swagger-ui.html"); + blackPatterns.add("/csrf"); + blackPatterns.add("/webjars/**"); + blackPatterns.add(uploadUrlPattern); + blackPatterns.add(adminPathPattern); + } + } } 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 d404101ac..f683761a4 100644 --- a/src/main/java/run/halo/app/config/properties/HaloProperties.java +++ b/src/main/java/run/halo/app/config/properties/HaloProperties.java @@ -61,6 +61,14 @@ public class HaloProperties { */ private Duration downloadTimeout = Duration.ofSeconds(30); + /** + * cache store impl + * memory + * level + */ + private String cache = "memory"; + + public HaloProperties() throws IOException { // Create work directory if not exist Files.createDirectories(Paths.get(workDir)); diff --git a/src/main/java/run/halo/app/controller/admin/api/AdminController.java b/src/main/java/run/halo/app/controller/admin/api/AdminController.java index 5035bfaa1..9a07b11af 100644 --- a/src/main/java/run/halo/app/controller/admin/api/AdminController.java +++ b/src/main/java/run/halo/app/controller/admin/api/AdminController.java @@ -16,7 +16,6 @@ import run.halo.app.security.token.AuthToken; import run.halo.app.service.AdminService; import run.halo.app.service.OptionService; -import javax.servlet.http.HttpServletResponse; import javax.validation.Valid; /** diff --git a/src/main/java/run/halo/app/controller/admin/api/DataProcessController.java b/src/main/java/run/halo/app/controller/admin/api/DataProcessController.java new file mode 100644 index 000000000..fccf379c5 --- /dev/null +++ b/src/main/java/run/halo/app/controller/admin/api/DataProcessController.java @@ -0,0 +1,50 @@ +package run.halo.app.controller.admin.api; + +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.*; +import run.halo.app.service.DataProcessService; +import run.halo.app.service.ThemeSettingService; + +/** + * @author ryanwang + * @date 2019-12-29 + */ +@RestController +@RequestMapping("/api/admin/data/process") +public class DataProcessController { + + private final DataProcessService dataProcessService; + + private final ThemeSettingService themeSettingService; + + public DataProcessController(DataProcessService dataProcessService, + ThemeSettingService themeSettingService) { + this.dataProcessService = dataProcessService; + this.themeSettingService = themeSettingService; + } + + @PutMapping("url/replace") + @ApiOperation("Replace url in all table.") + public void replaceUrl(@RequestParam("oldUrl") String oldUrl, + @RequestParam("newUrl") String newUrl) { + dataProcessService.replaceAllUrl(oldUrl, newUrl); + } + + @DeleteMapping("themes/settings/inactivated") + @ApiOperation("Delete inactivated theme settings.") + public void deleteInactivatedThemeSettings() { + themeSettingService.deleteInactivated(); + } + + @DeleteMapping("tags/unused") + @ApiOperation("Delete unused tags") + public void deleteUnusedTags() { + // TODO + } + + @DeleteMapping("categories/unused") + @ApiOperation("Delete unused categories") + public void deleteUnusedCategories() { + // TODO + } +} diff --git a/src/main/java/run/halo/app/controller/admin/api/StaticPageController.java b/src/main/java/run/halo/app/controller/admin/api/StaticPageController.java new file mode 100644 index 000000000..2a0b95a6f --- /dev/null +++ b/src/main/java/run/halo/app/controller/admin/api/StaticPageController.java @@ -0,0 +1,92 @@ +package run.halo.app.controller.admin.api; + +import cn.hutool.core.io.FileUtil; +import io.swagger.annotations.ApiOperation; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import run.halo.app.model.properties.NetlifyStaticDeployProperties; +import run.halo.app.model.support.StaticPageFile; +import run.halo.app.service.OptionService; +import run.halo.app.service.StaticPageService; + +import java.io.FileNotFoundException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +/** + * Static page controller. + * + * @author ryanwang + * @date 2019-12-25 + */ +@RestController +@RequestMapping("/api/admin/static_page") +public class StaticPageController { + + private final static String DEPLOY_API = "https://api.netlify.com/api/v1/sites/%s/deploys"; + + private final OptionService optionService; + + private final RestTemplate httpsRestTemplate; + + private final StaticPageService staticPageService; + + public StaticPageController(StaticPageService staticPageService, + OptionService optionService, + RestTemplate httpsRestTemplate) { + this.staticPageService = staticPageService; + this.optionService = optionService; + this.httpsRestTemplate = httpsRestTemplate; + + MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); + mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL)); + this.httpsRestTemplate.getMessageConverters().add(mappingJackson2HttpMessageConverter); + } + + @GetMapping + @ApiOperation("List static page files.") + public List list() { + return staticPageService.listFile(); + } + + @GetMapping("generate") + @ApiOperation("Generate static page files.") + public void generate() { + staticPageService.generate(); + } + + @PostMapping("deploy") + @ApiOperation("Deploy static page to remove platform") + public void deploy() { + staticPageService.deploy(); + } + + @GetMapping("netlify") + public void testNetlify() throws FileNotFoundException { + String domain = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_DOMAIN).toString(); + String siteId = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_SITE_ID).toString(); + String token = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_TOKEN).toString(); + + HttpHeaders headers = new HttpHeaders(); + + headers.set("Content-Type", "application/zip"); + headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token); + + Path path = staticPageService.zipStaticPagesDirectory(); + + byte[] bytes = FileUtil.readBytes(path.toFile()); + + HttpEntity httpEntity = new HttpEntity<>(bytes, headers); + + ResponseEntity responseEntity = httpsRestTemplate.postForEntity(String.format(DEPLOY_API, siteId), httpEntity, Object.class); + } +} diff --git a/src/main/java/run/halo/app/controller/content/ContentArchiveController.java b/src/main/java/run/halo/app/controller/content/ContentArchiveController.java index 7d0a2ae71..e734ffbf8 100644 --- a/src/main/java/run/halo/app/controller/content/ContentArchiveController.java +++ b/src/main/java/run/halo/app/controller/content/ContentArchiveController.java @@ -1,35 +1,19 @@ package run.halo.app.controller.content; import cn.hutool.core.util.IdUtil; -import cn.hutool.core.util.PageUtil; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.SortDefault; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import run.halo.app.cache.StringCacheStore; import run.halo.app.cache.lock.CacheLock; -import run.halo.app.exception.ForbiddenException; -import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Post; -import run.halo.app.model.entity.PostMeta; -import run.halo.app.model.entity.Tag; import run.halo.app.model.enums.PostStatus; -import run.halo.app.model.support.HaloConst; -import run.halo.app.model.vo.PostListVO; -import run.halo.app.service.*; -import run.halo.app.utils.MarkdownUtils; +import run.halo.app.service.OptionService; +import run.halo.app.service.PostService; -import java.util.List; import java.util.concurrent.TimeUnit; -import static org.springframework.data.domain.Sort.Direction.DESC; - /** * Blog archive page controller * @@ -45,129 +29,27 @@ public class ContentArchiveController { private final PostService postService; - private final ThemeService themeService; - - private final PostCategoryService postCategoryService; - - private final PostMetaService postMetaService; - - private final PostTagService postTagService; - private final OptionService optionService; private final StringCacheStore cacheStore; + public ContentArchiveController(PostService postService, - ThemeService themeService, - PostCategoryService postCategoryService, - PostMetaService postMetaService, - PostTagService postTagService, OptionService optionService, StringCacheStore cacheStore) { this.postService = postService; - this.themeService = themeService; - this.postCategoryService = postCategoryService; - this.postMetaService = postMetaService; - this.postTagService = postTagService; this.optionService = optionService; this.cacheStore = cacheStore; } - /** - * Render post archives page. - * - * @param model model - * @return template path : themes/{theme}/archives.ftl - */ - @GetMapping - public String archives(Model model) { - return this.archives(model, 1, Sort.by(DESC, "createTime")); - } - - /** - * Render post archives page. - * - * @param model model - * @return template path : themes/{theme}/archives.ftl - */ - @GetMapping(value = "page/{page}") - public String archives(Model model, - @PathVariable(value = "page") Integer page, - @SortDefault(sort = "createTime", direction = DESC) Sort sort) { - Pageable pageable = PageRequest.of(page - 1, optionService.getPostPageSize(), sort); - - Page postPage = postService.pageBy(PostStatus.PUBLISHED, pageable); - Page postListVos = postService.convertToListVo(postPage); - int[] pageRainbow = PageUtil.rainbow(page, postListVos.getTotalPages(), 3); - - model.addAttribute("is_archives", true); - model.addAttribute("pageRainbow", pageRainbow); - model.addAttribute("posts", postListVos); - - return themeService.render("archives"); - } - - /** - * Render post page. - * - * @param url post slug url. - * @param token view token. - * @param model model - * @return template path: themes/{theme}/post.ftl - */ - @GetMapping("{url}") - public String post(@PathVariable("url") String url, - @RequestParam(value = "token", required = false) String token, - Model model) { - Post post = postService.getByUrl(url); - - if (post.getStatus().equals(PostStatus.INTIMATE) && StringUtils.isEmpty(token)) { - String redirect = String.format("%s/archives/%s/password", optionService.getBlogBaseUrl(), post.getUrl()); - return "redirect:" + redirect; - } - - if (StringUtils.isEmpty(token)) { - post = postService.getBy(PostStatus.PUBLISHED, url); - } else { - // verify token - String cachedToken = cacheStore.getAny(token, String.class).orElseThrow(() -> new ForbiddenException("您没有该文章的访问权限")); - if (!cachedToken.equals(token)) { - throw new ForbiddenException("您没有该文章的访问权限"); - } - post.setFormatContent(MarkdownUtils.renderHtml(post.getOriginalContent())); - } - postService.publishVisitEvent(post.getId()); - postService.getNextPost(post.getCreateTime()).ifPresent(nextPost -> model.addAttribute("nextPost", nextPost)); - postService.getPrePost(post.getCreateTime()).ifPresent(prePost -> model.addAttribute("prePost", prePost)); - - List categories = postCategoryService.listCategoriesBy(post.getId()); - List tags = postTagService.listTagsBy(post.getId()); - List metas = postMetaService.listBy(post.getId()); - - model.addAttribute("is_post", true); - model.addAttribute("post", postService.convertToDetailVo(post)); - model.addAttribute("categories", categories); - model.addAttribute("tags", tags); - model.addAttribute("metas", postMetaService.convertToMap(metas)); - - // TODO,Will be deprecated - model.addAttribute("comments", Page.empty()); - - if (themeService.templateExists(ThemeService.CUSTOM_POST_PREFIX + post.getTemplate() + HaloConst.SUFFIX_FTL)) { - return themeService.render(ThemeService.CUSTOM_POST_PREFIX + post.getTemplate()); - } - - return themeService.render("post"); - } - - @GetMapping(value = "{url}/password") + @GetMapping(value = "{url:.*}/password") public String password(@PathVariable("url") String url, Model model) { model.addAttribute("url", url); return "common/template/post_password"; } - @PostMapping(value = "{url}/password") + @PostMapping(value = "{url:.*}/password") @CacheLock(traceRequest = true, expired = 2) public String password(@PathVariable("url") String url, @RequestParam(value = "password") String password) { diff --git a/src/main/java/run/halo/app/controller/content/ContentCategoryController.java b/src/main/java/run/halo/app/controller/content/ContentCategoryController.java deleted file mode 100644 index dc1fd70d8..000000000 --- a/src/main/java/run/halo/app/controller/content/ContentCategoryController.java +++ /dev/null @@ -1,104 +0,0 @@ -package run.halo.app.controller.content; - -import cn.hutool.core.util.PageUtil; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.SortDefault; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import run.halo.app.model.entity.Category; -import run.halo.app.model.entity.Post; -import run.halo.app.model.enums.PostStatus; -import run.halo.app.model.vo.PostListVO; -import run.halo.app.service.*; - -import static org.springframework.data.domain.Sort.Direction.DESC; - -/** - * Category controller. - * - * @author ryanwang - * @date 2019-03-20 - */ -@Controller -@RequestMapping(value = "/categories") -public class ContentCategoryController { - - private final CategoryService categoryService; - - private final ThemeService themeService; - - private final PostCategoryService postCategoryService; - - private final PostService postService; - - private final OptionService optionService; - - public ContentCategoryController(CategoryService categoryService, - ThemeService themeService, - PostCategoryService postCategoryService, - PostService postService, OptionService optionService) { - this.categoryService = categoryService; - this.themeService = themeService; - this.postCategoryService = postCategoryService; - this.postService = postService; - this.optionService = optionService; - } - - /** - * Render category list page - * - * @return template path: themes/{theme}/categories.ftl - */ - @GetMapping - public String categories(Model model) { - model.addAttribute("is_categories", true); - return themeService.render("categories"); - } - - /** - * Render post list page by category - * - * @param model model - * @param slugName slugName - * @return template path: themes/{theme}/category.ftl - */ - @GetMapping(value = "{slugName}") - public String categories(Model model, - @PathVariable("slugName") String slugName) { - return this.categories(model, slugName, 1, Sort.by(DESC, "createTime")); - } - - /** - * Render post list page by category - * - * @param model model - * @param slugName slugName - * @param page current page number - * @return template path: themes/{theme}/category.ftl - */ - @GetMapping("{slugName}/page/{page}") - public String categories(Model model, - @PathVariable("slugName") String slugName, - @PathVariable("page") Integer page, - @SortDefault(sort = "createTime", direction = DESC) Sort sort) { - // Get category by slug name - final Category category = categoryService.getBySlugNameOfNonNull(slugName); - - final Pageable pageable = PageRequest.of(page - 1, optionService.getPostPageSize(), sort); - Page postPage = postCategoryService.pagePostBy(category.getId(), PostStatus.PUBLISHED, pageable); - Page posts = postService.convertToListVo(postPage); - final int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3); - - model.addAttribute("is_category", true); - model.addAttribute("posts", posts); - model.addAttribute("rainbow", rainbow); - model.addAttribute("category", category); - return themeService.render("category"); - } -} diff --git a/src/main/java/run/halo/app/controller/content/ContentContentController.java b/src/main/java/run/halo/app/controller/content/ContentContentController.java new file mode 100644 index 000000000..83615753a --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/ContentContentController.java @@ -0,0 +1,165 @@ +package run.halo.app.controller.content; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import run.halo.app.controller.content.model.CategoryModel; +import run.halo.app.controller.content.model.PostModel; +import run.halo.app.controller.content.model.SheetModel; +import run.halo.app.controller.content.model.TagModel; +import run.halo.app.exception.NotFoundException; +import run.halo.app.model.entity.Post; +import run.halo.app.model.entity.Sheet; +import run.halo.app.model.enums.PostPermalinkType; +import run.halo.app.model.properties.PermalinkProperties; +import run.halo.app.service.OptionService; +import run.halo.app.service.PostService; +import run.halo.app.service.SheetService; + +/** + * @author ryanwang + * @date 2020-01-07 + */ +@Slf4j +@Controller +@RequestMapping +public class ContentContentController { + + private final PostModel postModel; + + private final SheetModel sheetModel; + + private final CategoryModel categoryModel; + + private final TagModel tagModel; + + private final OptionService optionService; + + private final PostService postService; + + private final SheetService sheetService; + + public ContentContentController(PostModel postModel, + SheetModel sheetModel, + CategoryModel categoryModel, + TagModel tagModel, + OptionService optionService, + PostService postService, + SheetService sheetService) { + this.postModel = postModel; + this.sheetModel = sheetModel; + this.categoryModel = categoryModel; + this.tagModel = tagModel; + this.optionService = optionService; + this.postService = postService; + this.sheetService = sheetService; + } + + @GetMapping("{prefix}") + public String content(@PathVariable("prefix") String prefix, + Model model) { + String archivesPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.ARCHIVES_PREFIX, String.class, PermalinkProperties.ARCHIVES_PREFIX.defaultValue()); + String categoriesPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.CATEGORIES_PREFIX, String.class, PermalinkProperties.CATEGORIES_PREFIX.defaultValue()); + String tagsPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.TAGS_PREFIX, String.class, PermalinkProperties.TAGS_PREFIX.defaultValue()); + + if (archivesPrefix.equals(prefix)) { + return postModel.list(1, model, "is_archives", "archives"); + } else if (categoriesPrefix.equals(prefix)) { + return categoryModel.list(model); + } else if (tagsPrefix.equals(prefix)) { + return tagModel.list(model); + } else { + throw new NotFoundException("Not Found"); + } + } + + @GetMapping("{prefix}/page/{page:\\d+}") + public String content(@PathVariable("prefix") String prefix, + @PathVariable(value = "page") Integer page, + Model model) { + String archivesPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.ARCHIVES_PREFIX, String.class, PermalinkProperties.ARCHIVES_PREFIX.defaultValue()); + if (archivesPrefix.equals(prefix)) { + return postModel.list(page, model, "is_archives", "archives"); + } else { + throw new NotFoundException("Not Found"); + } + } + + @GetMapping("{prefix}/{url:.+}") + public String content(@PathVariable("prefix") String prefix, + @PathVariable("url") String url, + @RequestParam(value = "token", required = false) String token, + Model model) { + PostPermalinkType postPermalinkType = optionService.getPostPermalinkType(); + String archivesPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.ARCHIVES_PREFIX, String.class, PermalinkProperties.ARCHIVES_PREFIX.defaultValue()); + String sheetPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.SHEET_PREFIX, String.class, PermalinkProperties.SHEET_PREFIX.defaultValue()); + String categoriesPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.CATEGORIES_PREFIX, String.class, PermalinkProperties.CATEGORIES_PREFIX.defaultValue()); + String tagsPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.TAGS_PREFIX, String.class, PermalinkProperties.TAGS_PREFIX.defaultValue()); + + if (postPermalinkType.equals(PostPermalinkType.DEFAULT) && archivesPrefix.equals(prefix)) { + Post post = postService.getByUrl(url); + return postModel.content(post, token, model); + } else if (sheetPrefix.equals(prefix)) { + Sheet sheet = sheetService.getByUrl(url); + return sheetModel.content(sheet, token, model); + } else if (categoriesPrefix.equals(prefix)) { + return categoryModel.listPost(model, url, 1); + } else if (tagsPrefix.equals(prefix)) { + return tagModel.listPost(model, url, 1); + } else { + throw new NotFoundException("Not Found"); + } + } + + @GetMapping("{prefix}/{url}/page/{page:\\d+}") + public String content(@PathVariable("prefix") String prefix, + @PathVariable("url") String url, + @PathVariable("page") Integer page, + Model model) { + String categoriesPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.CATEGORIES_PREFIX, String.class, PermalinkProperties.CATEGORIES_PREFIX.defaultValue()); + String tagsPrefix = optionService.getByPropertyOrDefault(PermalinkProperties.TAGS_PREFIX, String.class, PermalinkProperties.TAGS_PREFIX.defaultValue()); + + if (categoriesPrefix.equals(prefix)) { + return categoryModel.listPost(model, url, page); + } else if (tagsPrefix.equals(prefix)) { + return tagModel.listPost(model, url, page); + } else { + throw new NotFoundException("Not Found"); + } + } + + @GetMapping("{year:\\d+}/{month:\\d+}/{url:.+}") + public String content(@PathVariable("year") Integer year, + @PathVariable("month") Integer month, + @PathVariable("url") String url, + @RequestParam(value = "token", required = false) String token, + Model model) { + PostPermalinkType postPermalinkType = optionService.getPostPermalinkType(); + if (postPermalinkType.equals(PostPermalinkType.DATE)) { + Post post = postService.getBy(year, month, url); + return postModel.content(post, token, model); + } else { + throw new NotFoundException("Not Found"); + } + } + + @GetMapping("{year:\\d+}/{month:\\d+}/{day:\\d+}/{url:.+}") + public String content(@PathVariable("year") Integer year, + @PathVariable("month") Integer month, + @PathVariable("day") Integer day, + @PathVariable("url") String url, + @RequestParam(value = "token", required = false) String token, + Model model) { + PostPermalinkType postPermalinkType = optionService.getPostPermalinkType(); + if (postPermalinkType.equals(PostPermalinkType.DAY)) { + Post post = postService.getBy(year, month, day, url); + return postModel.content(post, token, model); + } else { + throw new NotFoundException("Not Found"); + } + } +} diff --git a/src/main/java/run/halo/app/controller/content/ContentIndexController.java b/src/main/java/run/halo/app/controller/content/ContentIndexController.java index 476184d98..555549a84 100644 --- a/src/main/java/run/halo/app/controller/content/ContentIndexController.java +++ b/src/main/java/run/halo/app/controller/content/ContentIndexController.java @@ -1,25 +1,18 @@ package run.halo.app.controller.content; -import cn.hutool.core.util.PageUtil; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import run.halo.app.controller.content.model.PostModel; import run.halo.app.model.entity.Post; -import run.halo.app.model.enums.PostStatus; -import run.halo.app.model.properties.PostProperties; -import run.halo.app.model.vo.PostListVO; +import run.halo.app.model.enums.PostPermalinkType; import run.halo.app.service.OptionService; import run.halo.app.service.PostService; -import run.halo.app.service.ThemeService; -import static org.springframework.data.domain.Sort.Direction.DESC; +import java.util.Objects; /** * Blog index page controller @@ -36,25 +29,34 @@ public class ContentIndexController { private final OptionService optionService; - private final ThemeService themeService; + private final PostModel postModel; public ContentIndexController(PostService postService, OptionService optionService, - ThemeService themeService) { + PostModel postModel) { this.postService = postService; this.optionService = optionService; - this.themeService = themeService; + this.postModel = postModel; } /** * Render blog index * + * @param p post id * @param model model * @return template path: themes/{theme}/index.ftl */ @GetMapping - public String index(Model model) { + public String index(Integer p, String token, Model model) { + + PostPermalinkType permalinkType = optionService.getPostPermalinkType(); + + if (PostPermalinkType.ID.equals(permalinkType) && !Objects.isNull(p)) { + Post post = postService.getById(p); + return postModel.content(post, token, model); + } + return this.index(model, 1); } @@ -68,18 +70,6 @@ public class ContentIndexController { @GetMapping(value = "page/{page}") public String index(Model model, @PathVariable(value = "page") Integer page) { - String indexSort = optionService.getByPropertyOfNonNull(PostProperties.INDEX_SORT).toString(); - int pageSize = optionService.getPostPageSize(); - Pageable pageable = PageRequest.of(page >= 1 ? page - 1 : page, pageSize, Sort.by(DESC, "topPriority").and(Sort.by(DESC, indexSort))); - - Page postPage = postService.pageBy(PostStatus.PUBLISHED, pageable); - Page posts = postService.convertToListVo(postPage); - - int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3); - - model.addAttribute("is_index", true); - model.addAttribute("posts", posts); - model.addAttribute("rainbow", rainbow); - return themeService.render("index"); + return postModel.list(page, model, "is_index", "index"); } } diff --git a/src/main/java/run/halo/app/controller/content/ContentSheetController.java b/src/main/java/run/halo/app/controller/content/ContentSheetController.java index 89c7e2e56..cca6dc329 100644 --- a/src/main/java/run/halo/app/controller/content/ContentSheetController.java +++ b/src/main/java/run/halo/app/controller/content/ContentSheetController.java @@ -1,6 +1,5 @@ package run.halo.app.controller.content; -import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -10,17 +9,9 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestParam; -import run.halo.app.cache.StringCacheStore; -import run.halo.app.exception.ForbiddenException; import run.halo.app.model.dto.PhotoDTO; -import run.halo.app.model.entity.Sheet; -import run.halo.app.model.enums.PostStatus; -import run.halo.app.model.support.HaloConst; -import run.halo.app.model.vo.SheetDetailVO; import run.halo.app.service.PhotoService; -import run.halo.app.service.SheetService; import run.halo.app.service.ThemeService; -import run.halo.app.utils.MarkdownUtils; import static org.springframework.data.domain.Sort.Direction.DESC; @@ -35,22 +26,14 @@ import static org.springframework.data.domain.Sort.Direction.DESC; public class ContentSheetController { - private final SheetService sheetService; - private final ThemeService themeService; private final PhotoService photoService; - private final StringCacheStore cacheStore; - - public ContentSheetController(SheetService sheetService, - ThemeService themeService, - PhotoService photoService, - StringCacheStore cacheStore) { - this.sheetService = sheetService; + public ContentSheetController(ThemeService themeService, + PhotoService photoService) { this.themeService = themeService; this.photoService = photoService; - this.cacheStore = cacheStore; } /** @@ -91,50 +74,4 @@ public class ContentSheetController { public String links() { return themeService.render("links"); } - - /** - * Render custom sheet - * - * @param url sheet url - * @param token view token - * @param model model - * @return template path: themes/{theme}/sheet.ftl - */ - @GetMapping(value = "/s/{url}") - public String sheet(@PathVariable(value = "url") String url, - @RequestParam(value = "token", required = false) String token, - Model model) { - - Sheet sheet = sheetService.getByUrl(url); - - if (StringUtils.isEmpty(token)) { - sheet = sheetService.getBy(PostStatus.PUBLISHED, url); - } else { - // render markdown to html when preview sheet - sheet.setFormatContent(MarkdownUtils.renderHtml(sheet.getOriginalContent())); - - // verify token - String cachedToken = cacheStore.getAny(token, String.class).orElseThrow(() -> new ForbiddenException("您没有该页面的访问权限")); - - if (!cachedToken.equals(token)) { - throw new ForbiddenException("您没有该页面的访问权限"); - } - } - sheetService.publishVisitEvent(sheet.getId()); - - SheetDetailVO sheetDetailVO = sheetService.convertToDetailVo(sheet); - - // sheet and post all can use - model.addAttribute("sheet", sheetDetailVO); - model.addAttribute("post", sheetDetailVO); - model.addAttribute("is_sheet", true); - - // TODO,Will be deprecated - model.addAttribute("comments", Page.empty()); - - if (themeService.templateExists(ThemeService.CUSTOM_SHEET_PREFIX + sheet.getTemplate() + HaloConst.SUFFIX_FTL)) { - return themeService.render(ThemeService.CUSTOM_SHEET_PREFIX + sheet.getTemplate()); - } - return themeService.render("sheet"); - } } diff --git a/src/main/java/run/halo/app/controller/content/ContentTagController.java b/src/main/java/run/halo/app/controller/content/ContentTagController.java deleted file mode 100644 index c5b62c03a..000000000 --- a/src/main/java/run/halo/app/controller/content/ContentTagController.java +++ /dev/null @@ -1,105 +0,0 @@ -package run.halo.app.controller.content; - -import cn.hutool.core.util.PageUtil; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.web.SortDefault; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import run.halo.app.model.entity.Post; -import run.halo.app.model.entity.Tag; -import run.halo.app.model.enums.PostStatus; -import run.halo.app.model.vo.PostListVO; -import run.halo.app.service.*; - -import static org.springframework.data.domain.Sort.Direction.DESC; - -/** - * Tag controller. - * - * @author ryanwang - * @date 2019-03-21 - */ -@Controller -@RequestMapping(value = "/tags") -public class ContentTagController { - - private final TagService tagService; - - private final PostService postService; - - private final PostTagService postTagService; - - private final OptionService optionService; - - private final ThemeService themeService; - - public ContentTagController(TagService tagService, - PostService postService, - PostTagService postTagService, - OptionService optionService, - ThemeService themeService) { - this.tagService = tagService; - this.postService = postService; - this.postTagService = postTagService; - this.optionService = optionService; - this.themeService = themeService; - } - - /** - * All of tags - * - * @return template path: themes/{theme}/tags.ftl - */ - @GetMapping - public String tags(Model model) { - model.addAttribute("is_tags", true); - return themeService.render("tags"); - } - - /** - * List tags by tag slug - * - * @param model model - * @param slugName slug name - * @return template path: themes/{theme}/tag.ftl - */ - @GetMapping(value = "{slugName}") - public String tags(Model model, - @PathVariable("slugName") String slugName) { - return this.tags(model, slugName, 1, Sort.by(DESC, "createTime")); - } - - /** - * List tags by tag slug - * - * @param model model - * @param slugName slug name - * @param page current page - * @return template path: themes/{theme}/tag.ftl - */ - @GetMapping(value = "{slugName}/page/{page}") - public String tags(Model model, - @PathVariable("slugName") String slugName, - @PathVariable("page") Integer page, - @SortDefault(sort = "createTime", direction = DESC) Sort sort) { - // Get tag by slug name - final Tag tag = tagService.getBySlugNameOfNonNull(slugName); - - final Pageable pageable = PageRequest.of(page - 1, optionService.getPostPageSize(), sort); - Page postPage = postTagService.pagePostsBy(tag.getId(), PostStatus.PUBLISHED, pageable); - Page posts = postService.convertToListVo(postPage); - final int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3); - - model.addAttribute("is_tag", true); - model.addAttribute("posts", posts); - model.addAttribute("rainbow", rainbow); - model.addAttribute("tag", tag); - return themeService.render("tag"); - } -} diff --git a/src/main/java/run/halo/app/controller/content/MainController.java b/src/main/java/run/halo/app/controller/content/MainController.java index e237b4ab2..ee122772a 100644 --- a/src/main/java/run/halo/app/controller/content/MainController.java +++ b/src/main/java/run/halo/app/controller/content/MainController.java @@ -12,7 +12,6 @@ import run.halo.app.model.support.HaloConst; import run.halo.app.service.OptionService; import run.halo.app.service.UserService; -import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @@ -48,7 +47,7 @@ public class MainController { } @GetMapping("${halo.admin-path:admin}") - public void admin(HttpServletRequest request, HttpServletResponse response) throws IOException { + public void admin(HttpServletResponse response) throws IOException { String adminIndexRedirectUri = StringUtils.appendIfMissing(this.haloProperties.getAdminPath(), "/") + INDEX_REDIRECT_URI; response.sendRedirect(adminIndexRedirectUri); } diff --git a/src/main/java/run/halo/app/controller/content/api/ArchiveController.java b/src/main/java/run/halo/app/controller/content/api/ArchiveController.java index 9c48e7d4c..a0da511a4 100644 --- a/src/main/java/run/halo/app/controller/content/api/ArchiveController.java +++ b/src/main/java/run/halo/app/controller/content/api/ArchiveController.java @@ -10,10 +10,10 @@ import run.halo.app.service.PostService; import java.util.List; /** - * Archive portal controller. + * Content archive controller. * * @author johnniang - * @date 4/2/19 + * @date 2019-04-02 */ @RestController("ApiContentArchiveController") @RequestMapping("/api/content/archives") diff --git a/src/main/java/run/halo/app/controller/content/api/CategoryController.java b/src/main/java/run/halo/app/controller/content/api/CategoryController.java index 2974ccd9e..b07c609db 100644 --- a/src/main/java/run/halo/app/controller/content/api/CategoryController.java +++ b/src/main/java/run/halo/app/controller/content/api/CategoryController.java @@ -21,10 +21,10 @@ import java.util.List; import static org.springframework.data.domain.Sort.Direction.DESC; /** - * Category portal controller. + * Content category controller. * * @author ryanwang - * @date 6/9/19 + * @date 2019-06-09 */ @RestController("ApiContentCategoryController") @RequestMapping("/api/content/categories") diff --git a/src/main/java/run/halo/app/controller/content/api/JournalController.java b/src/main/java/run/halo/app/controller/content/api/JournalController.java index d655825fb..d4ddfd9c2 100644 --- a/src/main/java/run/halo/app/controller/content/api/JournalController.java +++ b/src/main/java/run/halo/app/controller/content/api/JournalController.java @@ -29,7 +29,7 @@ import java.util.List; import static org.springframework.data.domain.Sort.Direction.DESC; /** - * Content Journal controller. + * Content journal controller. * * @author johnniang * @author ryanwang diff --git a/src/main/java/run/halo/app/controller/content/api/LinkController.java b/src/main/java/run/halo/app/controller/content/api/LinkController.java index 20b51b35f..80d7aae46 100644 --- a/src/main/java/run/halo/app/controller/content/api/LinkController.java +++ b/src/main/java/run/halo/app/controller/content/api/LinkController.java @@ -15,11 +15,11 @@ import java.util.List; import static org.springframework.data.domain.Sort.Direction.DESC; /** - * Portal link controller. + * Content link controller. * * @author johnniang * @author ryanwang - * @date 4/3/19 + * @date 2019-04-03 */ @RestController("ApiContentLinkController") @RequestMapping("/api/content/links") diff --git a/src/main/java/run/halo/app/controller/content/api/MenuController.java b/src/main/java/run/halo/app/controller/content/api/MenuController.java index 866cb5894..c03d748c9 100644 --- a/src/main/java/run/halo/app/controller/content/api/MenuController.java +++ b/src/main/java/run/halo/app/controller/content/api/MenuController.java @@ -15,11 +15,11 @@ import java.util.List; import static org.springframework.data.domain.Sort.Direction.DESC; /** - * Portal menu controller. + * Content menu controller. * * @author johnniang * @author ryanwang - * @date 4/3/19 + * @date 2019-04-03 */ @RestController("ApiContentMenuController") @RequestMapping("/api/content/menus") diff --git a/src/main/java/run/halo/app/controller/content/api/OptionController.java b/src/main/java/run/halo/app/controller/content/api/OptionController.java index 4ca8000db..940890920 100644 --- a/src/main/java/run/halo/app/controller/content/api/OptionController.java +++ b/src/main/java/run/halo/app/controller/content/api/OptionController.java @@ -13,10 +13,10 @@ import java.util.List; import java.util.Map; /** - * Portal option controller. + * Content option controller. * * @author johnniang - * @date 4/3/19 + * @date 2019-04-03 */ @RestController("ApiContentOptionController") @RequestMapping("/api/content/options") diff --git a/src/main/java/run/halo/app/controller/content/api/PostController.java b/src/main/java/run/halo/app/controller/content/api/PostController.java index de6622369..01191fad1 100644 --- a/src/main/java/run/halo/app/controller/content/api/PostController.java +++ b/src/main/java/run/halo/app/controller/content/api/PostController.java @@ -26,10 +26,10 @@ import java.util.List; import static org.springframework.data.domain.Sort.Direction.DESC; /** - * Portal post controller. + * Content post controller. * * @author johnniang - * @date 4/2/19 + * @date 2019-04-02 */ @RestController("ApiContentPostController") @RequestMapping("/api/content/posts") @@ -130,6 +130,7 @@ public class PostController { @ApiOperation("Comments a post") @CacheLock(autoDelete = false, traceRequest = true) public BaseCommentDTO comment(@RequestBody PostCommentParam postCommentParam) { + postCommentService.validateCommentBlackListStatus(); return postCommentService.convertTo(postCommentService.createBy(postCommentParam)); } diff --git a/src/main/java/run/halo/app/controller/content/api/SheetController.java b/src/main/java/run/halo/app/controller/content/api/SheetController.java index 29dce97f3..0e097be36 100644 --- a/src/main/java/run/halo/app/controller/content/api/SheetController.java +++ b/src/main/java/run/halo/app/controller/content/api/SheetController.java @@ -25,11 +25,11 @@ import java.util.List; import static org.springframework.data.domain.Sort.Direction.DESC; /** - * Sheet controller. + * Content sheet controller. * * @author johnniang * @author ryanwang - * @date 19-4-26 + * @date 2019-04-26 */ @RestController("ApiContentSheetController") @RequestMapping("/api/content/sheets") diff --git a/src/main/java/run/halo/app/controller/content/api/StatisticController.java b/src/main/java/run/halo/app/controller/content/api/StatisticController.java index 9faca5bf8..d4780cbaf 100644 --- a/src/main/java/run/halo/app/controller/content/api/StatisticController.java +++ b/src/main/java/run/halo/app/controller/content/api/StatisticController.java @@ -9,7 +9,7 @@ import run.halo.app.model.dto.StatisticWithUserDTO; import run.halo.app.service.StatisticService; /** - * Statistic controller. + * Content statistic controller. * * @author ryan0up * @date 2019-12-16 diff --git a/src/main/java/run/halo/app/controller/content/api/TagController.java b/src/main/java/run/halo/app/controller/content/api/TagController.java index ecf7e9ebb..4800f1b2d 100644 --- a/src/main/java/run/halo/app/controller/content/api/TagController.java +++ b/src/main/java/run/halo/app/controller/content/api/TagController.java @@ -22,11 +22,11 @@ import java.util.List; import static org.springframework.data.domain.Sort.Direction.DESC; /** - * Portal tag controller. + * Content tag controller. * * @author johnniang * @author ryanwang - * @date 4/2/19 + * @date 2019-04-02 */ @RestController("ApiContentTagController") @RequestMapping("/api/content/tags") diff --git a/src/main/java/run/halo/app/controller/content/api/ThemeController.java b/src/main/java/run/halo/app/controller/content/api/ThemeController.java new file mode 100644 index 000000000..37f8e6c57 --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/api/ThemeController.java @@ -0,0 +1,43 @@ +package run.halo.app.controller.content.api; + +import io.swagger.annotations.ApiOperation; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import run.halo.app.handler.theme.config.support.ThemeProperty; +import run.halo.app.service.ThemeService; +import run.halo.app.service.ThemeSettingService; + +import java.util.Map; + +/** + * Content theme controller. + * + * @author ryanwang + * @date 2020-01-17 + */ +@RestController("ApiContentThemeController") +@RequestMapping("/api/content/themes") +public class ThemeController { + + private final ThemeService themeService; + + private final ThemeSettingService themeSettingService; + + public ThemeController(ThemeService themeService, ThemeSettingService themeSettingService) { + this.themeService = themeService; + this.themeSettingService = themeSettingService; + } + + @GetMapping("activation") + @ApiOperation("Gets activated theme property") + public ThemeProperty getBy() { + return themeService.getThemeOfNonNullBy(themeService.getActivatedThemeId()); + } + + @GetMapping("activation/settings") + @ApiOperation("Lists activated theme settings") + public Map listSettingsBy() { + return themeSettingService.listAsMapBy(themeService.getActivatedThemeId()); + } +} diff --git a/src/main/java/run/halo/app/controller/content/api/UserController.java b/src/main/java/run/halo/app/controller/content/api/UserController.java index b9377e740..d7de6ec0b 100644 --- a/src/main/java/run/halo/app/controller/content/api/UserController.java +++ b/src/main/java/run/halo/app/controller/content/api/UserController.java @@ -8,10 +8,10 @@ import run.halo.app.model.dto.UserDTO; import run.halo.app.service.UserService; /** - * Portal user controller. + * Content user controller. * * @author johnniang - * @date 4/3/19 + * @date 2019-04-03 */ @RestController("ApiContentUserController") @RequestMapping("/api/content/users") diff --git a/src/main/java/run/halo/app/controller/content/model/CategoryModel.java b/src/main/java/run/halo/app/controller/content/model/CategoryModel.java new file mode 100644 index 000000000..ecc5aedf4 --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/model/CategoryModel.java @@ -0,0 +1,65 @@ +package run.halo.app.controller.content.model; + +import cn.hutool.core.util.PageUtil; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.ui.Model; +import run.halo.app.model.entity.Category; +import run.halo.app.model.entity.Post; +import run.halo.app.model.enums.PostStatus; +import run.halo.app.model.vo.PostListVO; +import run.halo.app.service.*; + +import static org.springframework.data.domain.Sort.Direction.DESC; + +/** + * Category Model. + * + * @author ryanwang + * @date 2020-01-11 + */ +@Component +public class CategoryModel { + + private final CategoryService categoryService; + + private final ThemeService themeService; + + private final PostCategoryService postCategoryService; + + private final PostService postService; + + private final OptionService optionService; + + public CategoryModel(CategoryService categoryService, ThemeService themeService, PostCategoryService postCategoryService, PostService postService, OptionService optionService) { + this.categoryService = categoryService; + this.themeService = themeService; + this.postCategoryService = postCategoryService; + this.postService = postService; + this.optionService = optionService; + } + + public String list(Model model) { + model.addAttribute("is_categories", true); + return themeService.render("categories"); + } + + public String listPost(Model model, String slugName, Integer page) { + // Get category by slug name + final Category category = categoryService.getBySlugNameOfNonNull(slugName); + + final Pageable pageable = PageRequest.of(page - 1, optionService.getPostPageSize(), Sort.by(DESC, "createTime")); + Page postPage = postCategoryService.pagePostBy(category.getId(), PostStatus.PUBLISHED, pageable); + Page posts = postService.convertToListVo(postPage); + final int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3); + + model.addAttribute("is_category", true); + model.addAttribute("posts", posts); + model.addAttribute("rainbow", rainbow); + model.addAttribute("category", category); + return themeService.render("category"); + } +} diff --git a/src/main/java/run/halo/app/controller/content/model/PostModel.java b/src/main/java/run/halo/app/controller/content/model/PostModel.java new file mode 100644 index 000000000..8848a49dd --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/model/PostModel.java @@ -0,0 +1,133 @@ +package run.halo.app.controller.content.model; + +import cn.hutool.core.util.PageUtil; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; +import org.springframework.ui.Model; +import run.halo.app.cache.StringCacheStore; +import run.halo.app.exception.ForbiddenException; +import run.halo.app.model.entity.Category; +import run.halo.app.model.entity.Post; +import run.halo.app.model.entity.PostMeta; +import run.halo.app.model.entity.Tag; +import run.halo.app.model.enums.PostStatus; +import run.halo.app.model.support.HaloConst; +import run.halo.app.model.vo.AdjacentPostVO; +import run.halo.app.model.vo.PostListVO; +import run.halo.app.service.*; +import run.halo.app.utils.MarkdownUtils; + +import java.util.List; + +/** + * Post Model + * + * @author ryanwang + * @date 2020-01-07 + */ +@Component +public class PostModel { + + private final PostService postService; + + private final ThemeService themeService; + + private final PostCategoryService postCategoryService; + + private final CategoryService categoryService; + + private final PostTagService postTagService; + + private final TagService tagService; + + private final PostMetaService postMetaService; + + private final OptionService optionService; + + private final StringCacheStore cacheStore; + + public PostModel(PostService postService, + ThemeService themeService, + PostCategoryService postCategoryService, + CategoryService categoryService, + PostMetaService postMetaService, + PostTagService postTagService, + TagService tagService, + OptionService optionService, + StringCacheStore cacheStore) { + this.postService = postService; + this.themeService = themeService; + this.postCategoryService = postCategoryService; + this.categoryService = categoryService; + this.postMetaService = postMetaService; + this.postTagService = postTagService; + this.tagService = tagService; + this.optionService = optionService; + this.cacheStore = cacheStore; + } + + public String content(Post post, String token, Model model) { + + if (post.getStatus().equals(PostStatus.INTIMATE) && StringUtils.isEmpty(token)) { + String redirect = String + .format("%s/archives/%s/password", optionService.getBlogBaseUrl(), + post.getUrl()); + return "redirect:" + redirect; + } + + if (!StringUtils.isEmpty(token)) { + // verify token + String cachedToken = cacheStore.getAny(token, String.class) + .orElseThrow(() -> new ForbiddenException("您没有该文章的访问权限")); + if (!cachedToken.equals(token)) { + throw new ForbiddenException("您没有该文章的访问权限"); + } + post.setFormatContent(MarkdownUtils.renderHtml(post.getOriginalContent())); + } + postService.publishVisitEvent(post.getId()); + + AdjacentPostVO adjacentPostVO = postService.getAdjacentPosts(post); + adjacentPostVO.getOptionalPrePost().ifPresent(prePost -> model.addAttribute("prePost", prePost)); + adjacentPostVO.getOptionalNextPost().ifPresent(nextPost -> model.addAttribute("nextPost", nextPost)); + + List categories = postCategoryService.listCategoriesBy(post.getId()); + List tags = postTagService.listTagsBy(post.getId()); + List metas = postMetaService.listBy(post.getId()); + + model.addAttribute("is_post", true); + model.addAttribute("post", postService.convertToDetailVo(post)); + model.addAttribute("categories", categoryService.convertTo(categories)); + model.addAttribute("tags", tagService.convertTo(tags)); + model.addAttribute("metas", postMetaService.convertToMap(metas)); + + // TODO,Will be deprecated + model.addAttribute("comments", Page.empty()); + + if (themeService.templateExists( + ThemeService.CUSTOM_POST_PREFIX + post.getTemplate() + HaloConst.SUFFIX_FTL)) { + return themeService.render(ThemeService.CUSTOM_POST_PREFIX + post.getTemplate()); + } + + return themeService.render("post"); + } + + public String list(Integer page, Model model, String decide, String template) { + int pageSize = optionService.getPostPageSize(); + Pageable pageable = PageRequest + .of(page >= 1 ? page - 1 : page, pageSize, postService.getPostDefaultSort()); + + Page postPage = postService.pageBy(PostStatus.PUBLISHED, pageable); + Page posts = postService.convertToListVo(postPage); + + int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3); + + model.addAttribute(decide, true); + model.addAttribute("posts", posts); + model.addAttribute("rainbow", rainbow); + model.addAttribute("pageRainbow", rainbow); + return themeService.render(template); + } +} diff --git a/src/main/java/run/halo/app/controller/content/model/SheetModel.java b/src/main/java/run/halo/app/controller/content/model/SheetModel.java new file mode 100644 index 000000000..7e0203e81 --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/model/SheetModel.java @@ -0,0 +1,66 @@ +package run.halo.app.controller.content.model; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; +import org.springframework.ui.Model; +import run.halo.app.cache.StringCacheStore; +import run.halo.app.exception.ForbiddenException; +import run.halo.app.model.entity.Sheet; +import run.halo.app.model.support.HaloConst; +import run.halo.app.model.vo.SheetDetailVO; +import run.halo.app.service.SheetService; +import run.halo.app.service.ThemeService; +import run.halo.app.utils.MarkdownUtils; + +/** + * Sheet model. + * + * @author ryanwang + * @date 2020-01-07 + */ +@Component +public class SheetModel { + + private final SheetService sheetService; + + private final StringCacheStore cacheStore; + + private final ThemeService themeService; + + public SheetModel(SheetService sheetService, StringCacheStore cacheStore, ThemeService themeService) { + this.sheetService = sheetService; + this.cacheStore = cacheStore; + this.themeService = themeService; + } + + public String content(Sheet sheet, String token, Model model) { + if (!StringUtils.isEmpty(token)) { + // render markdown to html when preview sheet + sheet.setFormatContent(MarkdownUtils.renderHtml(sheet.getOriginalContent())); + + // verify token + String cachedToken = cacheStore.getAny(token, String.class).orElseThrow(() -> new ForbiddenException("您没有该页面的访问权限")); + + if (!cachedToken.equals(token)) { + throw new ForbiddenException("您没有该页面的访问权限"); + } + } + sheetService.publishVisitEvent(sheet.getId()); + + SheetDetailVO sheetDetailVO = sheetService.convertToDetailVo(sheet); + + // sheet and post all can use + model.addAttribute("sheet", sheetDetailVO); + model.addAttribute("post", sheetDetailVO); + model.addAttribute("is_sheet", true); + + // TODO,Will be deprecated + model.addAttribute("comments", Page.empty()); + + if (themeService.templateExists(ThemeService.CUSTOM_SHEET_PREFIX + sheet.getTemplate() + HaloConst.SUFFIX_FTL)) { + return themeService.render(ThemeService.CUSTOM_SHEET_PREFIX + sheet.getTemplate()); + } + return themeService.render("sheet"); + } +} diff --git a/src/main/java/run/halo/app/controller/content/model/TagModel.java b/src/main/java/run/halo/app/controller/content/model/TagModel.java new file mode 100644 index 000000000..ae34ba2f5 --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/model/TagModel.java @@ -0,0 +1,65 @@ +package run.halo.app.controller.content.model; + +import cn.hutool.core.util.PageUtil; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.ui.Model; +import run.halo.app.model.entity.Post; +import run.halo.app.model.entity.Tag; +import run.halo.app.model.enums.PostStatus; +import run.halo.app.model.vo.PostListVO; +import run.halo.app.service.*; + +import static org.springframework.data.domain.Sort.Direction.DESC; + +/** + * Tag Model. + * + * @author ryanwang + * @date 2020-01-11 + */ +@Component +public class TagModel { + + private final TagService tagService; + + private final PostService postService; + + private final PostTagService postTagService; + + private final OptionService optionService; + + private final ThemeService themeService; + + public TagModel(TagService tagService, PostService postService, PostTagService postTagService, OptionService optionService, ThemeService themeService) { + this.tagService = tagService; + this.postService = postService; + this.postTagService = postTagService; + this.optionService = optionService; + this.themeService = themeService; + } + + public String list(Model model) { + model.addAttribute("is_tags", true); + return themeService.render("tags"); + } + + public String listPost(Model model, String slugName, Integer page) { + // Get tag by slug name + final Tag tag = tagService.getBySlugNameOfNonNull(slugName); + + final Pageable pageable = PageRequest.of(page - 1, optionService.getPostPageSize(), Sort.by(DESC, "createTime")); + Page postPage = postTagService.pagePostsBy(tag.getId(), PostStatus.PUBLISHED, pageable); + Page posts = postService.convertToListVo(postPage); + final int[] rainbow = PageUtil.rainbow(page, posts.getTotalPages(), 3); + + model.addAttribute("is_tag", true); + model.addAttribute("posts", posts); + model.addAttribute("rainbow", rainbow); + model.addAttribute("tag", tag); + return themeService.render("tag"); + } +} diff --git a/src/main/java/run/halo/app/core/freemarker/tag/ToolTagDirective.java b/src/main/java/run/halo/app/core/freemarker/tag/ToolTagDirective.java new file mode 100644 index 000000000..22fae23ca --- /dev/null +++ b/src/main/java/run/halo/app/core/freemarker/tag/ToolTagDirective.java @@ -0,0 +1,50 @@ +package run.halo.app.core.freemarker.tag; + +import cn.hutool.core.util.PageUtil; +import cn.hutool.core.util.RandomUtil; +import freemarker.core.Environment; +import freemarker.template.*; +import org.springframework.stereotype.Component; +import run.halo.app.model.support.HaloConst; + +import java.io.IOException; +import java.util.Map; + +/** + * Freemarker custom tag of tools. + * + * @author ryanwang + * @date 2020-01-17 + */ +@Component +public class ToolTagDirective implements TemplateDirectiveModel { + + public ToolTagDirective(Configuration configuration) { + configuration.setSharedVariable("toolTag", this); + } + + @Override + public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { + final DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_25); + + if (params.containsKey(HaloConst.METHOD_KEY)) { + String method = params.get(HaloConst.METHOD_KEY).toString(); + switch (method) { + case "rainbowPage": + int page = Integer.parseInt(params.get("page").toString()); + int total = Integer.parseInt(params.get("total").toString()); + int display = Integer.parseInt(params.get("display").toString()); + env.setVariable("numbers", builder.build().wrap(PageUtil.rainbow(page, total, display))); + break; + case "random": + int min = Integer.parseInt(params.get("min").toString()); + int max = Integer.parseInt(params.get("max").toString()); + env.setVariable("number", builder.build().wrap(RandomUtil.randomInt(min, max))); + break; + default: + break; + } + } + body.render(env.getOut()); + } +} diff --git a/src/main/java/run/halo/app/exception/ThemeNotSupportException.java b/src/main/java/run/halo/app/exception/ThemeNotSupportException.java new file mode 100644 index 000000000..1eabe73ad --- /dev/null +++ b/src/main/java/run/halo/app/exception/ThemeNotSupportException.java @@ -0,0 +1,18 @@ +package run.halo.app.exception; + +/** + * Theme not support exception. + * + * @author ryanwang + * @date 2020-02-03 + */ +public class ThemeNotSupportException extends BadRequestException { + + public ThemeNotSupportException(String message) { + super(message); + } + + public ThemeNotSupportException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/run/halo/app/handler/file/LocalFileHandler.java b/src/main/java/run/halo/app/handler/file/LocalFileHandler.java index e87797583..2dffe7722 100644 --- a/src/main/java/run/halo/app/handler/file/LocalFileHandler.java +++ b/src/main/java/run/halo/app/handler/file/LocalFileHandler.java @@ -19,6 +19,7 @@ import run.halo.app.utils.ImageUtils; import java.awt.image.BufferedImage; import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -142,14 +143,14 @@ public class LocalFileHandler implements FileHandler { // Check file type if (FileHandler.isImageType(uploadResult.getMediaType()) && !isSvg) { lock.lock(); - try { + try (InputStream uploadFileInputStream = new FileInputStream(uploadPath.toFile())) { // Upload a thumbnail String thumbnailBasename = basename + THUMBNAIL_SUFFIX; String thumbnailSubFilePath = subDir + thumbnailBasename + '.' + extension; Path thumbnailPath = Paths.get(workDir + thumbnailSubFilePath); // Read as image - BufferedImage originalImage = ImageUtils.getImageFromFile(new FileInputStream(uploadPath.toFile()), extension); + BufferedImage originalImage = ImageUtils.getImageFromFile(uploadFileInputStream, extension); // Set width and height uploadResult.setWidth(originalImage.getWidth()); uploadResult.setHeight(originalImage.getHeight()); @@ -232,7 +233,7 @@ public class LocalFileHandler implements FileHandler { log.debug("Generated thumbnail image, and wrote the thumbnail to [{}]", thumbPath.toString()); result = true; } catch (Throwable t) { - log.warn("Failed to generate thumbnail: [{}]", thumbPath); + log.warn("Failed to generate thumbnail: " + thumbPath, t); } return result; } diff --git a/src/main/java/run/halo/app/handler/migrate/CnBlogsMigrateHandler.java b/src/main/java/run/halo/app/handler/migrate/CnBlogsMigrateHandler.java index bdd512d6d..55f4f6930 100644 --- a/src/main/java/run/halo/app/handler/migrate/CnBlogsMigrateHandler.java +++ b/src/main/java/run/halo/app/handler/migrate/CnBlogsMigrateHandler.java @@ -1,5 +1,7 @@ package run.halo.app.handler.migrate; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import run.halo.app.model.enums.MigrateType; @@ -9,6 +11,8 @@ import run.halo.app.model.enums.MigrateType; * @author ryanwang * @date 2019-10-30 */ +@Slf4j +@Component public class CnBlogsMigrateHandler implements MigrateHandler { @Override diff --git a/src/main/java/run/halo/app/handler/migrate/MigrateHandlers.java b/src/main/java/run/halo/app/handler/migrate/MigrateHandlers.java index e7ac60f11..325c8fe92 100644 --- a/src/main/java/run/halo/app/handler/migrate/MigrateHandlers.java +++ b/src/main/java/run/halo/app/handler/migrate/MigrateHandlers.java @@ -31,7 +31,7 @@ public class MigrateHandlers { public MigrateHandlers(ApplicationContext applicationContext) { // Add all migrate handler - addFileHandlers(applicationContext.getBeansOfType(MigrateHandler.class).values()); + addMigrateHandlers(applicationContext.getBeansOfType(MigrateHandler.class).values()); } @NonNull @@ -56,7 +56,7 @@ public class MigrateHandlers { * @return current migrate handlers */ @NonNull - private MigrateHandlers addFileHandlers(@Nullable Collection migrateHandlers) { + private MigrateHandlers addMigrateHandlers(@Nullable Collection migrateHandlers) { if (!CollectionUtils.isEmpty(migrateHandlers)) { this.migrateHandlers.addAll(migrateHandlers); } diff --git a/src/main/java/run/halo/app/handler/migrate/WordPressMigrateHandler.java b/src/main/java/run/halo/app/handler/migrate/WordPressMigrateHandler.java index 2f38bdb6d..33b056ed2 100644 --- a/src/main/java/run/halo/app/handler/migrate/WordPressMigrateHandler.java +++ b/src/main/java/run/halo/app/handler/migrate/WordPressMigrateHandler.java @@ -1,34 +1,28 @@ package run.halo.app.handler.migrate; +import com.alibaba.fastjson.JSONObject; import lombok.extern.slf4j.Slf4j; -import org.dom4j.Element; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.FileCopyUtils; import org.springframework.web.multipart.MultipartFile; import run.halo.app.exception.ServiceException; -import run.halo.app.model.entity.BasePost; -import run.halo.app.model.entity.Category; -import run.halo.app.model.entity.Post; -import run.halo.app.model.entity.Tag; +import run.halo.app.handler.migrate.converter.Converter; +import run.halo.app.handler.migrate.converter.WordPressConverter; +import run.halo.app.handler.migrate.support.vo.PostVO; +import run.halo.app.handler.migrate.support.wordpress.Rss; import run.halo.app.model.enums.MigrateType; import run.halo.app.service.*; -import run.halo.app.utils.MarkdownUtils; -import run.halo.app.utils.WordPressMigrateUtils; +import run.halo.app.utils.XmlMigrateUtils; -import java.io.FileInputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.LinkedList; import java.util.List; -import java.util.Map; /** * WordPress migrate handler * * @author ryanwang + * @author guqing * @date 2019-10-28 */ @Slf4j @@ -90,138 +84,21 @@ public class WordPressMigrateHandler implements MigrateHandler { public void migrate(MultipartFile file) { try { String migrationContent = FileCopyUtils.copyToString(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8)); - Element rootElement = WordPressMigrateUtils.getRootElement(new FileInputStream(migrationContent)); - Map resultSetMapping = WordPressMigrateUtils.getResultSetMapping(rootElement); + String jsonString = XmlMigrateUtils.xml2jsonString(migrationContent); + JSONObject json = JSONObject.parseObject(jsonString); + Rss rss = json.getObject("rss", Rss.class); - // Handle categories - List categories = handleCategories(resultSetMapping.get("wp:category")); + // 转换 + Converter> converter = new WordPressConverter(); - // Handle tags - List tags = handleTags(resultSetMapping.get("wp:tag")); + List postVoList = converter.convertFrom(rss); - // Handle posts - List posts = handlePosts(resultSetMapping.get("item")); - - log.debug("Migrated posts: [{}]", posts); + log.debug("Migrated posts: [{}]", postVoList); } catch (Exception e) { - throw new ServiceException("WordPress 导出文件 " + file.getOriginalFilename() + " 读取失败", e); + throw new ServiceException("WordPress 入出文件 " + file.getOriginalFilename() + " 读取失败", e); } } - private List handleCategories(@Nullable Object categoriesObject) { - - if (!(categoriesObject instanceof List)) { - return Collections.emptyList(); - } - - List categoryObjectList = (List) categoriesObject; - - List result = new LinkedList<>(); - - categoryObjectList.forEach(categoryObject -> { - - if (!(categoryObject instanceof Map)) { - return; - } - - Map categoryMap = (Map) categoryObject; - - String slugName = categoryMap.getOrDefault("wp:category_nicename", "").toString(); - - Category category = categoryService.getBySlugName(slugName); - - if (null == category) { - category = new Category(); - category.setName(categoryMap.getOrDefault("wp:cat_name", "").toString()); - category.setSlugName(slugName); - category = categoryService.create(category); - } - - try { - result.add(category); - } catch (Exception e) { - log.warn("Failed to migrate a category", e); - } - }); - - return result; - } - - private List handleTags(@Nullable Object tagsObject) { - - if (!(tagsObject instanceof List)) { - return Collections.emptyList(); - } - - List tagObjectList = (List) tagsObject; - - List result = new LinkedList<>(); - - tagObjectList.forEach(tagObject -> { - if (!(tagObject instanceof Map)) { - return; - } - - Map tagMap = (Map) tagObject; - - String slugName = tagMap.getOrDefault("wp:tag_slug", "").toString(); - - Tag tag = tagService.getBySlugName(slugName); - - if (null == tag) { - tag = new Tag(); - tag.setName(tagMap.getOrDefault("wp:tag_name", "").toString()); - tag.setSlugName(slugName); - tag = tagService.create(tag); - } - - try { - result.add(tag); - } catch (Exception e) { - log.warn("Failed to migrate a tag", e); - } - }); - - return result; - } - - @NonNull - private List handlePosts(@Nullable Object postsObject) { - if (!(postsObject instanceof List)) { - return Collections.emptyList(); - } - - List postObjectList = (List) postsObject; - - List result = new LinkedList<>(); - - postObjectList.forEach(postObject -> { - if (!(postObject instanceof Map)) { - return; - } - - Map postMap = (Map) postObject; - - BasePost post = new BasePost(); - post.setTitle(postMap.getOrDefault("title", "").toString()); - post.setUrl(postMap.getOrDefault("wp:post_name", "").toString()); - post.setOriginalContent(MarkdownUtils.renderMarkdown(postMap.getOrDefault("content:encoded", "").toString())); - post.setFormatContent(postMap.getOrDefault("content:encoded", "").toString()); - post.setSummary(postMap.getOrDefault("excerpt:encoded", "").toString()); - - String url = postMap.getOrDefault("wp:post_name", "").toString(); - - Post temp = postService.getByUrl(url); - - if (temp != null) { - post.setUrl(post.getUrl() + "_1"); - } - - - }); - return null; - } - @Override public boolean supportType(MigrateType type) { return MigrateType.WORDPRESS.equals(type); diff --git a/src/main/java/run/halo/app/handler/migrate/converter/Converter.java b/src/main/java/run/halo/app/handler/migrate/converter/Converter.java new file mode 100644 index 000000000..aa45f8ec0 --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/converter/Converter.java @@ -0,0 +1,45 @@ +package run.halo.app.handler.migrate.converter; + +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 博客迁移数据转换器,只定义从SOURCE转TARGET的单向转换 + * + * @author guqing + * @date 2020-01-18 16:45 + */ +public interface Converter { + + /** + * 将source转换为target + * + * @param s 需要转换的源对象 + * @return 返回转换得到的结果对象 + */ + TARGET convertFrom(SOURCE s); + + /** + * 从SOURCE转为TARGET + * + * @param s source,需要转换的原始SOURCE集合 + * @param function 具体转换逻辑 + * @return 返回转换得到的TARGET对象 + */ + default TARGET convertFrom(SOURCE s, Function function) { + return function.apply(s); + } + + /** + * 批量从SOURCE转换得到TARGET + * + * @param list 需要转换的原始SOURCE集合 + * @param function 具体转换逻辑 + * @return 返回转换得到的TARGET集合结果 + */ + default List batchConverterFromDto(List list, Function function) { + return list.stream().map(s -> convertFrom(s, function)).collect(Collectors.toList()); + } +} + diff --git a/src/main/java/run/halo/app/handler/migrate/converter/WordPressConverter.java b/src/main/java/run/halo/app/handler/migrate/converter/WordPressConverter.java new file mode 100644 index 000000000..cc49a933d --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/converter/WordPressConverter.java @@ -0,0 +1,139 @@ +package run.halo.app.handler.migrate.converter; + +import cn.hutool.core.date.DateUtil; +import org.apache.commons.lang3.StringUtils; +import run.halo.app.handler.migrate.support.vo.PostVO; +import run.halo.app.handler.migrate.support.wordpress.*; +import run.halo.app.handler.migrate.utils.RelationMapperUtils; +import run.halo.app.model.entity.BaseComment; +import run.halo.app.model.entity.BasePost; +import run.halo.app.model.entity.Category; +import run.halo.app.model.entity.Tag; +import run.halo.app.utils.MarkdownUtils; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * WordPress 博客数据迁移转换器 + * + * @author guqing + * @author ryanwang + * @date 2020-01-18 16:50 + */ +public class WordPressConverter implements Converter> { + + @Override + public List convertFrom(Rss rss) { + return convertFrom(rss, this::apply); + } + + /** + * 自定义转换规则 + */ + public List apply(Rss rss) { + if (Objects.isNull(rss) || Objects.isNull(rss.getChannel())) { + return new ArrayList<>(); + } + + Channel channel = rss.getChannel(); + List items = channel.getItems(); + + if (items == null) { + return new ArrayList<>(); + } + return getBasePost(items); + } + + /** + * Gets post vo list from items. + * + * @param items wordpress items. + * @return a list of post vo. + */ + private List getBasePost(List items) { + List posts = new ArrayList<>(); + if (items == null) { + return posts; + } + + for (Item item : items) { + PostVO postVo = new PostVO(); + // 设置文章 + BasePost post = getBasePostFromItem(item); + postVo.setBasePost(post); + + // 获取标签和分类 + List categories = item.getCategories(); + List categoryModelList = new ArrayList<>(); + List tags = new ArrayList<>(); + if (categories != null) { + categories.forEach(category -> { + String domain = category.getDomain(); + if ("post_tag".equals(domain)) { + Tag tag = RelationMapperUtils.convertFrom(category, Tag.class); + tags.add(tag); + } else if ("category".equals(domain)) { + Category categoryModel = RelationMapperUtils.convertFrom(category, Category.class); + categoryModelList.add(categoryModel); + } + }); + } + // 设置标签和分类 + postVo.setCategories(categoryModelList); + postVo.setTags(tags); + + // 设置评论 + List comments = getCommentsFromItem(item); + postVo.setComments(comments); + + posts.add(postVo); + } + + return posts; + } + + /** + * Gets item's comments. + * + * @param item wordpress item. + * @return a list of baseComment. + */ + private List getCommentsFromItem(Item item) { + List baseComments = new ArrayList<>(); + if (Objects.isNull(item) || Objects.isNull(item.getComments())) { + return baseComments; + } + + List comments = item.getComments(); + for (Comment comment : comments) { + BaseComment baseComment = RelationMapperUtils.convertFrom(comment, BaseComment.class); + Date commentDate = DateUtil.parseDateTime(comment.getCommentDate()); + baseComment.setCreateTime(commentDate); + baseComment.setUpdateTime(commentDate); + baseComments.add(baseComment); + } + + return baseComments; + } + + /** + * Gets base post from item. + * + * @param item wordpress item. + * @return base post. + */ + private BasePost getBasePostFromItem(Item item) { + BasePost post = RelationMapperUtils.convertFrom(item, BasePost.class); + Date postDate = DateUtil.parseDateTime(item.getPostDate()); + if (StringUtils.isNoneEmpty(post.getFormatContent())) { + post.setOriginalContent(MarkdownUtils.renderMarkdown(post.getFormatContent())); + } + post.setCreateTime(postDate); + post.setUpdateTime(postDate); + post.setEditTime(postDate); + return post; + } +} diff --git a/src/main/java/run/halo/app/handler/migrate/support/vo/PostVO.java b/src/main/java/run/halo/app/handler/migrate/support/vo/PostVO.java new file mode 100644 index 000000000..fa01ceb40 --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/support/vo/PostVO.java @@ -0,0 +1,21 @@ +package run.halo.app.handler.migrate.support.vo; + +import lombok.Data; +import run.halo.app.model.entity.BaseComment; +import run.halo.app.model.entity.BasePost; +import run.halo.app.model.entity.Category; +import run.halo.app.model.entity.Tag; + +import java.util.List; + +/** + * @author guqing + * @date 2020-01-18 16:52 + */ +@Data +public class PostVO { + private BasePost basePost; + private List tags; + private List categories; + private List comments; +} diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/Channel.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Channel.java new file mode 100644 index 000000000..fc1d5f47f --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Channel.java @@ -0,0 +1,28 @@ +package run.halo.app.handler.migrate.support.wordpress; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +import java.util.List; + +/** + *

+ * WordPress导出的xml中对应的channel节点下的子节点将被映射为该类的属性 + *

+ * + * @author guqing + * @date 2019-11-17 13:57 + */ +@Data +public class Channel { + @JSONField(name = "title") + private String title; + private String link; + private String description; + private String pubDate; + private String language; + private String author; + + @JSONField(name = "item") + private List items; +} diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/Comment.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Comment.java new file mode 100644 index 000000000..cf3c33448 --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Comment.java @@ -0,0 +1,56 @@ +package run.halo.app.handler.migrate.support.wordpress; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import run.halo.app.handler.migrate.utils.PropertyMappingTo; + +/** + *

+ * WordPress导出的xml中对于的comment节点下的子节点值将被映射为该类的属性, + * 最终会被转换为{@link run.halo.app.model.entity.PostComment} + *

+ * + * @author guqing + * @author ryanwang + * @date 2019-11-17 14:01 + */ +@Data +public class Comment { + + @JSONField(name = "wp:comment_id") + @PropertyMappingTo("id") + private String commentId; + + @JSONField(name = "wp:comment_author") + @PropertyMappingTo("author") + private String commentAuthor; + + @JSONField(name = "wp:comment_author_email") + @PropertyMappingTo("email") + private String commentAuthorEmail; + + @JSONField(name = "wp:comment_author_url") + @PropertyMappingTo("authorUrl") + private String commentAuthorUrl; + + @JSONField(name = "wp:comment_author_IP") + @PropertyMappingTo("ipAddress") + private String commentAuthorIp; + + @JSONField(name = "wp:comment_date") + private String commentDate; + + @JSONField(name = "wp:comment_content") + @PropertyMappingTo("content") + private String commentContent; + + @JSONField(name = "wp:comment_approved") + private String commentApproved; + + @JSONField(name = "wp:comment_parent") + @PropertyMappingTo("parentId") + private Long commentParent; + + @JSONField(name = "wp:comment_user_id") + private String commentUserId; +} diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/Item.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Item.java new file mode 100644 index 000000000..e81c4c297 --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Item.java @@ -0,0 +1,60 @@ +package run.halo.app.handler.migrate.support.wordpress; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import run.halo.app.handler.migrate.utils.PropertyMappingTo; +import run.halo.app.model.entity.BasePost; + +import java.util.List; + +/** + *

WordPress导出的xml中对于的item子节点的值将会被映射到该类的属性上,最终被解析为文章属性{@link BasePost}

+ * + * @author guqing + * @author ryanwang + * @date 2019-11-17 13:59 + */ +@Data +public class Item { + + private String title; + + private String pubDate; + + private String description; + + @JSONField(name = "content:encoded") + @PropertyMappingTo("formatContent") + private String content; + + @JSONField(name = "excerpt:encoded") + @PropertyMappingTo("summary") + private String excerpt; + + @JSONField(name = "wp:post_date") + private String postDate; + + @JSONField(name = "wp:comment_status") + private String commentStatus; + + @JSONField(name = "wp:post_name") + @PropertyMappingTo("url") + private String postName; + + @JSONField(name = "wp:status") + private String status; + + @JSONField(name = "wp:post_password") + @PropertyMappingTo("password") + private String postPassword; + + @JSONField(name = "wp:is_sticky") + @PropertyMappingTo("topPriority") + private Integer isSticky; + + @JSONField(name = "wp:comment") + private List comments; + + @JSONField(name = "category") + private List categories; +} diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/Rss.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Rss.java new file mode 100644 index 000000000..c1f84a5f7 --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/Rss.java @@ -0,0 +1,15 @@ +package run.halo.app.handler.migrate.support.wordpress; + +import lombok.Data; + +/** + * WordPress导出的xml数据中对应的rss节点下的子节点channel将会被映射到该类属性 + * 如果不写这个Rss类包装想要的Channel,会无法解析到想要的结果 + * + * @author guqing + * @date 2020-01-17 00:45 + */ +@Data +public class Rss { + private Channel channel; +} diff --git a/src/main/java/run/halo/app/handler/migrate/support/wordpress/WpCategory.java b/src/main/java/run/halo/app/handler/migrate/support/wordpress/WpCategory.java new file mode 100644 index 000000000..e020c318a --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/support/wordpress/WpCategory.java @@ -0,0 +1,23 @@ +package run.halo.app.handler.migrate.support.wordpress; + +import lombok.Data; +import run.halo.app.handler.migrate.utils.PropertyMappingTo; +import run.halo.app.model.entity.PostCategory; + +/** + * WordPress导出的xml数据中对应的category的子节点将会被映射到该类属性, + * 最终会被转换为文章分类{@link PostCategory} + * + * @author guqing + * @date 2020-01-18 16:09 + */ +@Data +public class WpCategory { + private String domain; + + @PropertyMappingTo("slugName") + private String nicename; + + @PropertyMappingTo("name") + private String content; +} diff --git a/src/main/java/run/halo/app/handler/migrate/utils/PropertyMappingTo.java b/src/main/java/run/halo/app/handler/migrate/utils/PropertyMappingTo.java new file mode 100644 index 000000000..4b6cb5e2d --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/utils/PropertyMappingTo.java @@ -0,0 +1,21 @@ +package run.halo.app.handler.migrate.utils; + +import java.lang.annotation.*; + +/** + * 该注解用于定义两个对象之间的属性映射关系 + * + * @author guqing + * @date 2020-1-19 13:51 + */ +@Target({ElementType.FIELD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface PropertyMappingTo { + /** + * value对应的是目标对象的属性名称 + * + * @return 返回源对象属性对应的目标对象的属性名 + */ + String value() default ""; +} diff --git a/src/main/java/run/halo/app/handler/migrate/utils/RelationMapperUtils.java b/src/main/java/run/halo/app/handler/migrate/utils/RelationMapperUtils.java new file mode 100644 index 000000000..a5c799dd3 --- /dev/null +++ b/src/main/java/run/halo/app/handler/migrate/utils/RelationMapperUtils.java @@ -0,0 +1,128 @@ +package run.halo.app.handler.migrate.utils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + * 关系映射工具类,用于使用@PropertyMappingTo注解映射源对象与目标对象属性之间的关系 + * + * @author guqing + * @date 2020-01-19 01:47 + */ +public class RelationMapperUtils { + private RelationMapperUtils() { + } + + public static TARGET convertFrom(SOURCE source, Class targetClazz) { + Map methodNameAndTypeMap = new HashMap<>(16); + TARGET target; + try { + // 通过类的详情信息,创建目标对象 这一步等同于Hello target = new Hello(); + target = targetClazz.getConstructor().newInstance(); + Field[] targetFields = targetClazz.getDeclaredFields(); + for (Field field : targetFields) { + methodNameAndTypeMap.put(field.getName(), field.getType()); + } + } catch (Exception e) { + throw new RuntimeException("目标对象创建失败"); + } + + return propertyMapper(source, target, methodNameAndTypeMap); + } + + private static TARGET propertyMapper(SOURCE source, TARGET target, Map methodNameAndTypeMap) { + // 判断传入源数据是否为空,如果空,则抛自定义异常 + if (null == source) { + throw new IllegalArgumentException("数据源不能为空"); + } + // 获取源对象的类的详情信息 + Class sourceClazz = source.getClass(); + // 获取源对象的所有属性 + Field[] sourceFields = sourceClazz.getDeclaredFields(); + + // 循环取到源对象的单个属性 + for (Field sourceField : sourceFields) { + boolean sourceFieldHasAnnotation = sourceField.isAnnotationPresent(PropertyMappingTo.class); + + String sourceFieldName = sourceField.getName(); + // 如果源对象没有添加注解则默认使用源对象属性名去寻找对于的目标对象属性名 + if (sourceFieldHasAnnotation) { + PropertyMappingTo propertyMapping = sourceField.getAnnotation(PropertyMappingTo.class); + String targetFieldName = propertyMapping.value(); + copyProperty(sourceFieldName, targetFieldName, source, target, sourceField.getType(), methodNameAndTypeMap); + } else if (methodNameAndTypeMap.containsKey(sourceFieldName)) { + // 如果源对象和目标对象的属性名相同则直接设置 + // 获取目标对象的属性名,将属性名首字母大写,拼接如:setUsername、setId的字符串 + // 判断源对象的属性名、属性类型是否和目标对象的属性名、属性类型一致 + copyProperty(sourceFieldName, sourceFieldName, source, target, sourceField.getType(), methodNameAndTypeMap); + } + } + // 返回赋值得到的目标对象 + return target; + } + + private static void copyProperty(String sourceFieldName, String targetFieldName, + SOURCE source, TARGET target, Class sourceType, + Map methodNameAndTypeMap) { + try { + Class sourceClazz = source.getClass(); + Class targetClazz = target.getClass(); + // 获取源对象的属性名,将属性名首字母大写,拼接如:getUsername、getId的字符串 + String sourceGetMethodName = getterName(sourceFieldName); + // 获得属性的get方法 + Method sourceMethod = sourceClazz.getMethod(sourceGetMethodName); + // 调用get方法 + Object sourceFieldValue = sourceMethod.invoke(source); + + Class methodType = methodNameAndTypeMap.get(targetFieldName); + // 通过字段名称得到set方法名称 + String targetSetMethodName = setterName(targetFieldName); + if (methodType == sourceType) { + // 调用方法,并将源对象get方法返回值作为参数传入 + Method targetSetMethod = targetClazz.getMethod(targetSetMethodName, sourceType); + targetSetMethod.invoke(target, sourceFieldValue); + } + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException("转换失败,请检查属性类型是否匹配"); + } + } + + /** + * 通过属性名称得到对于的get或set方法 + * + * @param fieldName 属性名称 + * @param methodType 方法类型可选值为:get或set + * @return 返回对象的get或set方法的名称 + */ + private static String getMethodNameFromField(String fieldName, String methodType) { + if (fieldName == null || fieldName.length() == 0) { + return null; + } + + /* + * If the second char is upper, make 'get' + field name as getter name. For example, eBlog -> getBlog + */ + if (fieldName.length() > 2) { + String second = fieldName.substring(1, 2); + if (second.equals(second.toUpperCase())) { + return methodType + fieldName; + } + } + + // Common situation + fieldName = methodType + fieldName.substring(0, 1).toUpperCase() + + fieldName.substring(1); + return fieldName; + } + + private static String getterName(String fieldName) { + return getMethodNameFromField(fieldName, "get"); + } + + private static String setterName(String fieldName) { + return getMethodNameFromField(fieldName, "set"); + } +} diff --git a/src/main/java/run/halo/app/handler/staticdeploy/GitStaticDeployHandler.java b/src/main/java/run/halo/app/handler/staticdeploy/GitStaticDeployHandler.java new file mode 100644 index 000000000..0b94b0059 --- /dev/null +++ b/src/main/java/run/halo/app/handler/staticdeploy/GitStaticDeployHandler.java @@ -0,0 +1,33 @@ +package run.halo.app.handler.staticdeploy; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import run.halo.app.model.enums.StaticDeployType; +import run.halo.app.service.OptionService; + +/** + * Git deploy handler. + * + * @author ryanwang + * @date 2019-12-26 + */ +@Slf4j +@Component +public class GitStaticDeployHandler implements StaticDeployHandler { + + private final OptionService optionService; + + public GitStaticDeployHandler(OptionService optionService) { + this.optionService = optionService; + } + + @Override + public void deploy() { + + } + + @Override + public boolean supportType(StaticDeployType type) { + return StaticDeployType.GIT.equals(type); + } +} diff --git a/src/main/java/run/halo/app/handler/staticdeploy/NetlifyStaticDeployHandler.java b/src/main/java/run/halo/app/handler/staticdeploy/NetlifyStaticDeployHandler.java new file mode 100644 index 000000000..dddad86e1 --- /dev/null +++ b/src/main/java/run/halo/app/handler/staticdeploy/NetlifyStaticDeployHandler.java @@ -0,0 +1,67 @@ +package run.halo.app.handler.staticdeploy; + +import cn.hutool.core.io.FileUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import run.halo.app.model.enums.StaticDeployType; +import run.halo.app.model.properties.NetlifyStaticDeployProperties; +import run.halo.app.service.OptionService; +import run.halo.app.service.StaticPageService; + +import java.nio.file.Path; + +/** + * Netlify deploy handler. + * + * @author ryanwang + * @date 2019-12-26 + */ +@Slf4j +@Component +public class NetlifyStaticDeployHandler implements StaticDeployHandler { + + private final static String DEPLOY_API = "https://api.netlify.com/api/v1/sites/%s/deploys"; + + private final OptionService optionService; + + private final RestTemplate httpsRestTemplate; + + private final StaticPageService staticPageService; + + public NetlifyStaticDeployHandler(OptionService optionService, + RestTemplate httpsRestTemplate, + StaticPageService staticPageService) { + this.optionService = optionService; + this.httpsRestTemplate = httpsRestTemplate; + this.staticPageService = staticPageService; + } + + @Override + public void deploy() { + String domain = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_DOMAIN).toString(); + String siteId = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_SITE_ID).toString(); + String token = optionService.getByPropertyOfNonNull(NetlifyStaticDeployProperties.NETLIFY_TOKEN).toString(); + + HttpHeaders headers = new HttpHeaders(); + + headers.set("Content-Type", "application/zip"); + headers.set(HttpHeaders.AUTHORIZATION, "Bearer " + token); + + Path path = staticPageService.zipStaticPagesDirectory(); + + byte[] bytes = FileUtil.readBytes(path.toFile()); + + HttpEntity httpEntity = new HttpEntity<>(bytes, headers); + + ResponseEntity responseEntity = httpsRestTemplate.postForEntity(String.format(DEPLOY_API, siteId), httpEntity, Object.class); + } + + @Override + public boolean supportType(StaticDeployType type) { + return StaticDeployType.NETLIFY.equals(type); + } +} diff --git a/src/main/java/run/halo/app/handler/staticdeploy/StaticDeployHandler.java b/src/main/java/run/halo/app/handler/staticdeploy/StaticDeployHandler.java new file mode 100644 index 000000000..f4da08737 --- /dev/null +++ b/src/main/java/run/halo/app/handler/staticdeploy/StaticDeployHandler.java @@ -0,0 +1,26 @@ +package run.halo.app.handler.staticdeploy; + +import org.springframework.lang.Nullable; +import run.halo.app.model.enums.StaticDeployType; + +/** + * Static deploy handler interface class. + * + * @author ryanwang + * @date 2019-12-26 + */ +public interface StaticDeployHandler { + + /** + * do deploy. + */ + void deploy(); + + /** + * Checks if the given type is supported. + * + * @param type deploy type + * @return true if supported; false or else + */ + boolean supportType(@Nullable StaticDeployType type); +} diff --git a/src/main/java/run/halo/app/handler/staticdeploy/StaticDeployHandlers.java b/src/main/java/run/halo/app/handler/staticdeploy/StaticDeployHandlers.java new file mode 100644 index 000000000..4a8b8138f --- /dev/null +++ b/src/main/java/run/halo/app/handler/staticdeploy/StaticDeployHandlers.java @@ -0,0 +1,65 @@ +package run.halo.app.handler.staticdeploy; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationContext; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import run.halo.app.exception.FileOperationException; +import run.halo.app.model.enums.StaticDeployType; + +import java.util.Collection; +import java.util.LinkedList; + +/** + * Static deploy handlers. + * + * @author ryanwang + * @date 2019-12-26 + */ +@Slf4j +@Component +public class StaticDeployHandlers { + + private final Collection staticDeployHandlers = new LinkedList<>(); + + public StaticDeployHandlers(ApplicationContext applicationContext) { + // Add all static deploy handler + addStaticDeployHandlers(applicationContext.getBeansOfType(StaticDeployHandler.class).values()); + } + + + /** + * do deploy. + * + * @param staticDeployType static deploy type + */ + public void deploy(@NonNull StaticDeployType staticDeployType) { + Assert.notNull(staticDeployType, "Static deploy type must not be null"); + + for (StaticDeployHandler staticDeployHandler : staticDeployHandlers) { + if (staticDeployHandler.supportType(staticDeployType)) { + staticDeployHandler.deploy(); + return; + } + } + + throw new FileOperationException("No available static deploy handler to deploy static pages").setErrorData(staticDeployType); + } + + /** + * Adds static deploy handlers. + * + * @param staticDeployHandlers static deploy handler collection + * @return current file handlers + */ + @NonNull + public StaticDeployHandlers addStaticDeployHandlers(@Nullable Collection staticDeployHandlers) { + if (!CollectionUtils.isEmpty(staticDeployHandlers)) { + this.staticDeployHandlers.addAll(staticDeployHandlers); + } + return this; + } +} diff --git a/src/main/java/run/halo/app/handler/theme/config/support/ThemeProperty.java b/src/main/java/run/halo/app/handler/theme/config/support/ThemeProperty.java index 784e97cef..eaf5722b8 100644 --- a/src/main/java/run/halo/app/handler/theme/config/support/ThemeProperty.java +++ b/src/main/java/run/halo/app/handler/theme/config/support/ThemeProperty.java @@ -54,6 +54,11 @@ public class ThemeProperty { */ private String version; + /** + * Require halo version. + */ + private String require; + /** * Theme author. */ diff --git a/src/main/java/run/halo/app/listener/freemarker/FreemarkerConfigAwareListener.java b/src/main/java/run/halo/app/listener/freemarker/FreemarkerConfigAwareListener.java index 50c519a95..2738876e0 100644 --- a/src/main/java/run/halo/app/listener/freemarker/FreemarkerConfigAwareListener.java +++ b/src/main/java/run/halo/app/listener/freemarker/FreemarkerConfigAwareListener.java @@ -10,8 +10,10 @@ import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import run.halo.app.event.options.OptionUpdatedEvent; import run.halo.app.event.theme.ThemeActivatedEvent; +import run.halo.app.event.theme.ThemeUpdatedEvent; import run.halo.app.event.user.UserUpdatedEvent; import run.halo.app.handler.theme.config.support.ThemeProperty; +import run.halo.app.model.properties.OtherProperties; import run.halo.app.model.support.HaloConst; import run.halo.app.service.OptionService; import run.halo.app.service.ThemeService; @@ -22,7 +24,8 @@ import run.halo.app.service.UserService; * Freemarker config aware listener. * * @author johnniang - * @date 19-4-20 + * @author ryanwang + * @date 2019-04-20 */ @Slf4j @Component @@ -67,6 +70,13 @@ public class FreemarkerConfigAwareListener { loadThemeConfig(); } + @EventListener + public void onThemeUpdatedEvent(ThemeUpdatedEvent event) throws TemplateModelException { + log.debug("Received theme updated event"); + + loadThemeConfig(); + } + @EventListener public void onUserUpdate(UserUpdatedEvent event) throws TemplateModelException { log.debug("Received user updated event, user id: [{}]", event.getUserId()); @@ -96,9 +106,21 @@ public class FreemarkerConfigAwareListener { } private void loadThemeConfig() throws TemplateModelException { + + // Get current activated theme. ThemeProperty activatedTheme = themeService.getActivatedTheme(); + + Boolean enabledAbsolutePath = optionService.getByPropertyOrDefault(OtherProperties.GLOBAL_ABSOLUTE_PATH_ENABLED, Boolean.class, true); + + String themeBasePath = (enabledAbsolutePath ? optionService.getBlogBaseUrl() : "") + "/themes/" + activatedTheme.getFolderName(); + configuration.setSharedVariable("theme", activatedTheme); - configuration.setSharedVariable("static", optionService.getBlogBaseUrl() + "/" + activatedTheme.getFolderName()); + + // TODO: It will be removed in future versions + configuration.setSharedVariable("static", themeBasePath); + + configuration.setSharedVariable("theme_base", themeBasePath); + configuration.setSharedVariable("settings", themeSettingService.listAsMapBy(themeService.getActivatedThemeId())); log.debug("Loaded theme and settings"); } diff --git a/src/main/java/run/halo/app/model/dto/CategoryDTO.java b/src/main/java/run/halo/app/model/dto/CategoryDTO.java index 2f835c05c..d16ea1a2d 100644 --- a/src/main/java/run/halo/app/model/dto/CategoryDTO.java +++ b/src/main/java/run/halo/app/model/dto/CategoryDTO.java @@ -12,7 +12,8 @@ import java.util.Date; * Category output dto. * * @author johnniang - * @date 3/19/19 + * @author ryanwang + * @date 2019-03-19 */ @Data @ToString @@ -30,4 +31,6 @@ public class CategoryDTO implements OutputConverter { private Integer parentId; private Date createTime; + + private String fullPath; } diff --git a/src/main/java/run/halo/app/model/dto/TagDTO.java b/src/main/java/run/halo/app/model/dto/TagDTO.java index 2e871c7f8..4498b6d18 100644 --- a/src/main/java/run/halo/app/model/dto/TagDTO.java +++ b/src/main/java/run/halo/app/model/dto/TagDTO.java @@ -10,7 +10,8 @@ import java.util.Date; * Tag output dto. * * @author johnniang - * @date 3/19/19 + * @author ryanwang + * @date 2019-03-19 */ @Data public class TagDTO implements OutputConverter { @@ -22,4 +23,6 @@ public class TagDTO implements OutputConverter { private String slugName; private Date createTime; + + private String fullPath; } diff --git a/src/main/java/run/halo/app/model/dto/post/BasePostMinimalDTO.java b/src/main/java/run/halo/app/model/dto/post/BasePostMinimalDTO.java index a9b482484..cc3619cfe 100644 --- a/src/main/java/run/halo/app/model/dto/post/BasePostMinimalDTO.java +++ b/src/main/java/run/halo/app/model/dto/post/BasePostMinimalDTO.java @@ -13,6 +13,8 @@ import java.util.Date; * Base post minimal output dto. * * @author johnniang + * @author ryanwang + * @date 2019-03-19 */ @Data @ToString @@ -32,4 +34,6 @@ public class BasePostMinimalDTO implements OutputConverter { + + /** + * Relative path. + */ + RELATIVE(0), + + /** + * Absolute path. + */ + ABSOLUTE(1); + + private Integer value; + + GlobalPathType(Integer value) { + this.value = value; + } + + @Override + public Integer getValue() { + return value; + } +} diff --git a/src/main/java/run/halo/app/model/enums/PostPermalinkType.java b/src/main/java/run/halo/app/model/enums/PostPermalinkType.java new file mode 100644 index 000000000..e0ae85854 --- /dev/null +++ b/src/main/java/run/halo/app/model/enums/PostPermalinkType.java @@ -0,0 +1,42 @@ +package run.halo.app.model.enums; + +/** + * Post Permalink type enum. + * + * @author ryanwang + * @date 2020-01-07 + */ +public enum PostPermalinkType implements ValueEnum { + + /** + * /archives/${url} + */ + DEFAULT(0), + + /** + * /1970/01/01/${url} + */ + DATE(1), + + /** + * /1970/01/${url} + */ + DAY(2), + + /** + * /?p=${id} + */ + ID(3); + + private Integer value; + + PostPermalinkType(Integer value) { + this.value = value; + } + + + @Override + public Integer getValue() { + return value; + } +} diff --git a/src/main/java/run/halo/app/model/enums/StaticDeployType.java b/src/main/java/run/halo/app/model/enums/StaticDeployType.java new file mode 100644 index 000000000..4dcd9a50d --- /dev/null +++ b/src/main/java/run/halo/app/model/enums/StaticDeployType.java @@ -0,0 +1,36 @@ +package run.halo.app.model.enums; + +/** + * Static deploy type. + * + * @author ryanwang + * @date 2019-12-26 + */ +public enum StaticDeployType implements ValueEnum { + + /** + * Deploy static pages in remote git repository, such as github pages,gitee pages,coding pages.etc. + */ + GIT(0), + + /** + * Deploy static pages in netlify. + */ + NETLIFY(1); + + private Integer value; + + StaticDeployType(Integer value) { + this.value = value; + } + + /** + * Get enum value. + * + * @return enum value + */ + @Override + public Integer getValue() { + return value; + } +} diff --git a/src/main/java/run/halo/app/model/properties/CommentProperties.java b/src/main/java/run/halo/app/model/properties/CommentProperties.java index 1d622b4e9..f08aab9e8 100644 --- a/src/main/java/run/halo/app/model/properties/CommentProperties.java +++ b/src/main/java/run/halo/app/model/properties/CommentProperties.java @@ -25,7 +25,12 @@ public enum CommentProperties implements PropertyEnum { CONTENT_PLACEHOLDER("comment_content_placeholder", String.class, ""), - INTERNAL_PLUGIN_JS("comment_internal_plugin_js", String.class, "//cdn.jsdelivr.net/gh/halo-dev/halo-comment@latest/dist/halo-comment.min.js"); + INTERNAL_PLUGIN_JS("comment_internal_plugin_js", String.class, "//cdn.jsdelivr.net/gh/halo-dev/halo-comment@latest/dist/halo-comment.min.js"), + + COMMENT_BAN_TIME("comment_ban_time", Integer.class, "10"), + + COMMENT_RANGE("comment_range", Integer.class, "30"); + private final String value; diff --git a/src/main/java/run/halo/app/model/properties/GitStaticDeployProperties.java b/src/main/java/run/halo/app/model/properties/GitStaticDeployProperties.java new file mode 100644 index 000000000..e52bdc12a --- /dev/null +++ b/src/main/java/run/halo/app/model/properties/GitStaticDeployProperties.java @@ -0,0 +1,76 @@ +package run.halo.app.model.properties; + +/** + * Git static deploy properties. + * + * @author ryanwang + * @date 2019-12-26 + */ +public enum GitStaticDeployProperties implements PropertyEnum { + + /** + * Git static deploy domain. + */ + GIT_DOMAIN("git_static_deploy_domain", String.class, ""), + + /** + * Git static deploy repository. + */ + GIT_REPOSITORY("git_static_deploy_repository", String.class, ""), + + /** + * Git static deploy branch. + */ + GIT_BRANCH("git_static_deploy_branch", String.class, "master"), + + /** + * Git static deploy username. + */ + GIT_USERNAME("git_static_deploy_username", String.class, ""), + + /** + * Git static deploy email. + */ + GIT_EMAIL("git_static_deploy_email", String.class, ""), + + /** + * Git static deploy token. + */ + GIT_TOKEN("git_static_deploy_token", String.class, ""), + + /** + * Git static deploy cname. + */ + GIT_CNAME("git_static_deploy_cname", String.class, ""); + + private final String value; + + private final Class type; + + private final String defaultValue; + + GitStaticDeployProperties(String value, Class type, String defaultValue) { + this.defaultValue = defaultValue; + if (!PropertyEnum.isSupportedType(type)) { + throw new IllegalArgumentException("Unsupported blog property type: " + type); + } + + this.value = value; + this.type = type; + } + + @Override + public Class getType() { + return type; + } + + @Override + public String defaultValue() { + return defaultValue; + } + + @Override + public String getValue() { + return value; + } +} diff --git a/src/main/java/run/halo/app/model/properties/NetlifyStaticDeployProperties.java b/src/main/java/run/halo/app/model/properties/NetlifyStaticDeployProperties.java new file mode 100644 index 000000000..ac5574859 --- /dev/null +++ b/src/main/java/run/halo/app/model/properties/NetlifyStaticDeployProperties.java @@ -0,0 +1,56 @@ +package run.halo.app.model.properties; + +/** + * Netlify static deploy properties. + * + * @author ryanwang + * @date 2019-12-26 + */ +public enum NetlifyStaticDeployProperties implements PropertyEnum { + + /** + * Netlify static deploy domain. + */ + NETLIFY_DOMAIN("netlify_static_deploy_domain", String.class, ""), + + /** + * Netlify static deploy site id. + */ + NETLIFY_SITE_ID("netlify_static_deploy_site_id", String.class, ""), + + /** + * Netlify static deploy token. + */ + NETLIFY_TOKEN("netlify_static_deploy_token", String.class, ""); + + private final String value; + + private final Class type; + + private final String defaultValue; + + NetlifyStaticDeployProperties(String value, Class type, String defaultValue) { + this.defaultValue = defaultValue; + if (!PropertyEnum.isSupportedType(type)) { + throw new IllegalArgumentException("Unsupported blog property type: " + type); + } + + this.value = value; + this.type = type; + } + + @Override + public Class getType() { + return type; + } + + @Override + public String defaultValue() { + return defaultValue; + } + + @Override + public String getValue() { + return value; + } +} diff --git a/src/main/java/run/halo/app/model/properties/OtherProperties.java b/src/main/java/run/halo/app/model/properties/OtherProperties.java index 89d7547a5..6c57a7eeb 100644 --- a/src/main/java/run/halo/app/model/properties/OtherProperties.java +++ b/src/main/java/run/halo/app/model/properties/OtherProperties.java @@ -22,7 +22,12 @@ public enum OtherProperties implements PropertyEnum { /** * Statistics platform code,such as Google Analytics. */ - STATISTICS_CODE("blog_statistics_code", String.class, ""); + STATISTICS_CODE("blog_statistics_code", String.class, ""), + + /** + * Global absolute path enabled. + */ + GLOBAL_ABSOLUTE_PATH_ENABLED("global_absolute_path_enabled", Boolean.class, "true"); private final String value; diff --git a/src/main/java/run/halo/app/model/properties/PermalinkProperties.java b/src/main/java/run/halo/app/model/properties/PermalinkProperties.java new file mode 100644 index 000000000..5745566f7 --- /dev/null +++ b/src/main/java/run/halo/app/model/properties/PermalinkProperties.java @@ -0,0 +1,74 @@ +package run.halo.app.model.properties; + +import run.halo.app.model.enums.PostPermalinkType; + +/** + * Permalink properties enum. + * + * @author ryanwang + * @date 2020-01-07 + */ +public enum PermalinkProperties implements PropertyEnum { + + /** + * Post Permalink type. + */ + POST_PERMALINK_TYPE("post_permalink_type", PostPermalinkType.class, PostPermalinkType.DEFAULT.name()), + + /** + * Categories prefix + * such as: /categories or /categories/${slugName} + */ + CATEGORIES_PREFIX("categories_prefix", String.class, "categories"), + + /** + * Tags prefix + * such as: /tags or /tags/${slugName} + */ + TAGS_PREFIX("tags_prefix", String.class, "tags"), + + /** + * Archives prefix. + * such as: /archives + */ + ARCHIVES_PREFIX("archives_prefix", String.class, "archives"), + + /** + * Sheet prefix + * such as: /s/${url} + */ + SHEET_PREFIX("sheet_prefix", String.class, "s"), + + /** + * Path suffix + * such as: .html or .jsp + */ + PATH_SUFFIX("path_suffix", String.class, ""); + + private final String value; + + private final Class type; + + private final String defaultValue; + + PermalinkProperties(String value, Class type, String defaultValue) { + this.value = value; + this.type = type; + this.defaultValue = defaultValue; + } + + @Override + public Class getType() { + return type; + } + + @Override + public String defaultValue() { + return defaultValue; + } + + @Override + public String getValue() { + return value; + } +} diff --git a/src/main/java/run/halo/app/model/properties/PropertyEnum.java b/src/main/java/run/halo/app/model/properties/PropertyEnum.java index 65a57290e..240053224 100644 --- a/src/main/java/run/halo/app/model/properties/PropertyEnum.java +++ b/src/main/java/run/halo/app/model/properties/PropertyEnum.java @@ -155,6 +155,10 @@ public interface PropertyEnum extends ValueEnum { propertyEnumClasses.add(SeoProperties.class); propertyEnumClasses.add(UpOssProperties.class); propertyEnumClasses.add(ApiProperties.class); + propertyEnumClasses.add(StaticDeployProperties.class); + propertyEnumClasses.add(GitStaticDeployProperties.class); + propertyEnumClasses.add(NetlifyStaticDeployProperties.class); + propertyEnumClasses.add(PermalinkProperties.class); Map result = new HashMap<>(); diff --git a/src/main/java/run/halo/app/model/properties/StaticDeployProperties.java b/src/main/java/run/halo/app/model/properties/StaticDeployProperties.java new file mode 100644 index 000000000..f1222bed5 --- /dev/null +++ b/src/main/java/run/halo/app/model/properties/StaticDeployProperties.java @@ -0,0 +1,45 @@ +package run.halo.app.model.properties; + +import run.halo.app.model.enums.StaticDeployType; + +/** + * Static deploy properties. + * + * @author ryanwang + * @date 2019-12-26 + */ +public enum StaticDeployProperties implements PropertyEnum { + + /** + * static deploy type + */ + DEPLOY_TYPE("static_deploy_type", StaticDeployType.class, StaticDeployType.GIT.name()); + + private final String value; + + private final Class type; + + private final String defaultValue; + + + StaticDeployProperties(String value, Class type, String defaultValue) { + this.value = value; + this.type = type; + this.defaultValue = defaultValue; + } + + @Override + public Class getType() { + return type; + } + + @Override + public String defaultValue() { + return defaultValue; + } + + @Override + public String getValue() { + return value; + } +} 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 c4e34a704..d8f45327b 100644 --- a/src/main/java/run/halo/app/model/support/HaloConst.java +++ b/src/main/java/run/halo/app/model/support/HaloConst.java @@ -30,6 +30,11 @@ public class HaloConst { */ public final static String HALO_BACKUP_PREFIX = "halo-backup-"; + /** + * Static pages pack prefix. + */ + public final static String STATIC_PAGE_PACK_PREFIX = "static-pages-"; + /** * Default theme name. */ diff --git a/src/main/java/run/halo/app/model/support/StaticFile.java b/src/main/java/run/halo/app/model/support/StaticFile.java index e1de4146a..c7f05e693 100644 --- a/src/main/java/run/halo/app/model/support/StaticFile.java +++ b/src/main/java/run/halo/app/model/support/StaticFile.java @@ -16,6 +16,8 @@ import java.util.List; @ToString public class StaticFile implements Comparator { + private String id; + private String name; private String path; diff --git a/src/main/java/run/halo/app/model/support/StaticPageFile.java b/src/main/java/run/halo/app/model/support/StaticPageFile.java new file mode 100644 index 000000000..f92139d1a --- /dev/null +++ b/src/main/java/run/halo/app/model/support/StaticPageFile.java @@ -0,0 +1,37 @@ +package run.halo.app.model.support; + +import lombok.Data; + +import java.util.Comparator; +import java.util.List; + +/** + * Static page dto. + * + * @author ryanwang + * @date 2019-12-26 + */ +@Data +public class StaticPageFile implements Comparator { + + private String id; + + private String name; + + private Boolean isFile; + + private List children; + + @Override + public int compare(StaticPageFile leftFile, StaticPageFile rightFile) { + if (leftFile.isFile && !rightFile.isFile) { + return 1; + } + + if (!leftFile.isFile && rightFile.isFile) { + return -1; + } + + return leftFile.getName().compareTo(rightFile.getName()); + } +} diff --git a/src/main/java/run/halo/app/model/vo/AdjacentPostVO.java b/src/main/java/run/halo/app/model/vo/AdjacentPostVO.java new file mode 100644 index 000000000..21337c068 --- /dev/null +++ b/src/main/java/run/halo/app/model/vo/AdjacentPostVO.java @@ -0,0 +1,34 @@ +package run.halo.app.model.vo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import run.halo.app.model.entity.Post; + +import java.util.Optional; + +/** + * AdjacentPost class + * + * @author zhouchunjie + * @date 2020/1/12 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AdjacentPostVO { + + private Post prePost; + private Post nextPost; + + public Optional getOptionalPrePost() { + return Optional.ofNullable(this.getPrePost()); + } + + public Optional getOptionalNextPost() { + return Optional.ofNullable(this.getNextPost()); + } + +} diff --git a/src/main/java/run/halo/app/model/vo/PostListVO.java b/src/main/java/run/halo/app/model/vo/PostListVO.java index 80db07fb0..5b8d51870 100644 --- a/src/main/java/run/halo/app/model/vo/PostListVO.java +++ b/src/main/java/run/halo/app/model/vo/PostListVO.java @@ -14,6 +14,7 @@ import java.util.List; * * @author johnniang * @author guqing + * @author ryanwang * @date 2019-03-19 */ @EqualsAndHashCode(callSuper = true) diff --git a/src/main/java/run/halo/app/repository/CommentBlackListRepository.java b/src/main/java/run/halo/app/repository/CommentBlackListRepository.java new file mode 100644 index 000000000..54b664d0c --- /dev/null +++ b/src/main/java/run/halo/app/repository/CommentBlackListRepository.java @@ -0,0 +1,36 @@ +package run.halo.app.repository; + +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import run.halo.app.model.entity.CommentBlackList; +import run.halo.app.repository.base.BaseRepository; + +import java.util.Optional; + +/** + * 评论黑名单Repository + * + * @author Lei XinXin + * @date 2020/1/3 + */ +public interface CommentBlackListRepository extends BaseRepository { + + /** + * 根据IP地址获取数据 + * + * @param ipAddress + * @return + */ + Optional findByIpAddress(String ipAddress); + + /** + * Update Comment BlackList By IPAddress + * + * @param commentBlackList + * @return result + */ + @Modifying + @Query("UPDATE CommentBlackList SET banTime=:#{#commentBlackList.banTime} WHERE ipAddress=:#{#commentBlackList.ipAddress}") + int updateByIpAddress(@Param("commentBlackList") CommentBlackList commentBlackList); +} diff --git a/src/main/java/run/halo/app/repository/PostCommentRepository.java b/src/main/java/run/halo/app/repository/PostCommentRepository.java index 2109e4e71..37ec1878a 100644 --- a/src/main/java/run/halo/app/repository/PostCommentRepository.java +++ b/src/main/java/run/halo/app/repository/PostCommentRepository.java @@ -8,6 +8,7 @@ import run.halo.app.model.projection.CommentCountProjection; import run.halo.app.repository.base.BaseCommentRepository; import java.util.Collection; +import java.util.Date; import java.util.List; /** @@ -45,4 +46,15 @@ public interface PostCommentRepository extends BaseCommentRepository findDirectChildrenCount(@NonNull Collection commentIds); + + /** + * 根据时间范围和IP地址统计评论次数 + * + * @param ipAddress IP地址 + * @param startTime 起始时间 + * @param endTime 结束时间 + * @return 评论次数 + */ + @Query("SELECT COUNT(id) FROM PostComment WHERE ipAddress=?1 AND updateTime BETWEEN ?2 AND ?3 AND status <> 2") + int countByIpAndTime(String ipAddress, Date startTime, Date endTime); } diff --git a/src/main/java/run/halo/app/repository/PostRepository.java b/src/main/java/run/halo/app/repository/PostRepository.java index 6064bf491..1c522f6c0 100644 --- a/src/main/java/run/halo/app/repository/PostRepository.java +++ b/src/main/java/run/halo/app/repository/PostRepository.java @@ -2,15 +2,20 @@ package run.halo.app.repository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import run.halo.app.model.entity.Post; +import run.halo.app.model.enums.PostStatus; import run.halo.app.repository.base.BasePostRepository; +import java.util.Optional; + /** * Post repository. * * @author johnniang * @author ryanwang + * @date 2019-03-19 */ public interface PostRepository extends BasePostRepository, JpaSpecificationExecutor { @@ -32,4 +37,51 @@ public interface PostRepository extends BasePostRepository, JpaSpecificati @Query("select sum(p.likes) from Post p") Long countLike(); + /** + * Find by post year and month and url. + * + * @param year post create year + * @param month post create month + * @param url post url + * @return a optional of post + */ + @Query("select post from Post post where DateUtil.year(post.createTime) = :year and DateUtil.month(post.createTime) = :month and post.url = :url") + Optional findBy(@Param("year") Integer year, @Param("month") Integer month, @Param("url") String url); + + /** + * Find by post year and month and url and status. + * + * @param year post create year + * @param month post create month + * @param url post url + * @param status post status + * @return a optional of post + */ + @Query("select post from Post post where DateUtil.year(post.createTime) = :year and DateUtil.month(post.createTime) = :month and post.url = :url and post.status = :status") + Optional findBy(@Param("year") Integer year, @Param("month") Integer month, @Param("url") String url, @Param("status") PostStatus status); + + /** + * Find by post year and month and day and url. + * + * @param year post create year + * @param month post create month + * @param day post create day + * @param url post url + * @return a optional of post + */ + @Query("select post from Post post where DateUtil.year(post.createTime) = :year and DateUtil.month(post.createTime) = :month and DateUtil.dayOfMonth(post.createTime) = :day and post.url = :url") + Optional findBy(@Param("year") Integer year, @Param("month") Integer month, @Param("day") Integer day, @Param("url") String url); + + /** + * Find by post year and month and day and url and status. + * + * @param year post create year + * @param month post create month + * @param day post create day + * @param url post url + * @param status post status + * @return a optional of post + */ + @Query("select post from Post post where DateUtil.year(post.createTime) = :year and DateUtil.month(post.createTime) = :month and DateUtil.dayOfMonth(post.createTime) = :day and post.url = :url and post.status = :status") + Optional findBy(@Param("year") Integer year, @Param("month") Integer month, @Param("day") Integer day, @Param("url") String url, @Param("status") PostStatus status); } diff --git a/src/main/java/run/halo/app/repository/ThemeSettingRepository.java b/src/main/java/run/halo/app/repository/ThemeSettingRepository.java index 8ea8b50e3..9a504c199 100644 --- a/src/main/java/run/halo/app/repository/ThemeSettingRepository.java +++ b/src/main/java/run/halo/app/repository/ThemeSettingRepository.java @@ -42,4 +42,11 @@ public interface ThemeSettingRepository extends BaseRepository findByThemeIdAndKey(@NonNull String themeId, @NonNull String key); + + /** + * Deletes inactivated theme settings. + * + * @param activatedThemeId activated theme id. + */ + void deleteByThemeIdIsNot(@NonNull String activatedThemeId); } diff --git a/src/main/java/run/halo/app/repository/base/BasePostRepository.java b/src/main/java/run/halo/app/repository/base/BasePostRepository.java index 733034e39..396060ee8 100644 --- a/src/main/java/run/halo/app/repository/base/BasePostRepository.java +++ b/src/main/java/run/halo/app/repository/base/BasePostRepository.java @@ -18,6 +18,7 @@ import java.util.Optional; * Base post repository. * * @author johnniang + * @author ryanwang * @date 2019-03-22 */ public interface BasePostRepository extends BaseRepository { @@ -99,6 +100,16 @@ public interface BasePostRepository extends BaseRepositor @NonNull Optional getByUrlAndStatus(@NonNull String url, @NonNull PostStatus status); + /** + * Gets post by id and status. + * + * @param id id must not be blank + * @param status status must not be null + * @return an optional post + */ + @NonNull + Optional getByIdAndStatus(@NonNull Integer id, @NonNull PostStatus status); + /** * Counts posts by status and type. diff --git a/src/main/java/run/halo/app/service/AdminService.java b/src/main/java/run/halo/app/service/AdminService.java index d3533af26..2a3426664 100644 --- a/src/main/java/run/halo/app/service/AdminService.java +++ b/src/main/java/run/halo/app/service/AdminService.java @@ -7,8 +7,6 @@ import run.halo.app.model.params.LoginParam; import run.halo.app.model.params.ResetPasswordParam; import run.halo.app.security.token.AuthToken; -import javax.servlet.http.HttpServletResponse; - /** * Admin service interface. * diff --git a/src/main/java/run/halo/app/service/AttachmentService.java b/src/main/java/run/halo/app/service/AttachmentService.java index a57b360f3..ec35d13db 100644 --- a/src/main/java/run/halo/app/service/AttachmentService.java +++ b/src/main/java/run/halo/app/service/AttachmentService.java @@ -83,4 +83,13 @@ public interface AttachmentService extends CrudService { * @return list of type. */ List listAllType(); + + /** + * Replace attachment url in batch. + * + * @param oldUrl old blog url. + * @param newUrl new blog url. + * @return replaced attachments. + */ + List replaceUrl(@NonNull String oldUrl, @NonNull String newUrl); } diff --git a/src/main/java/run/halo/app/service/CommentBlackListService.java b/src/main/java/run/halo/app/service/CommentBlackListService.java new file mode 100644 index 000000000..703cc91b7 --- /dev/null +++ b/src/main/java/run/halo/app/service/CommentBlackListService.java @@ -0,0 +1,19 @@ +package run.halo.app.service; + +import run.halo.app.model.enums.CommentViolationTypeEnum; + +/** + * Comment BlackList Service + * + * @author Lei XinXin + * @date 2020/1/3 + */ +public interface CommentBlackListService { + /** + * 评论封禁状态 + * + * @param ipAddress ip地址 + * @return boolean + */ + CommentViolationTypeEnum commentsBanStatus(String ipAddress); +} diff --git a/src/main/java/run/halo/app/service/DataProcessService.java b/src/main/java/run/halo/app/service/DataProcessService.java new file mode 100644 index 000000000..adf9d253d --- /dev/null +++ b/src/main/java/run/halo/app/service/DataProcessService.java @@ -0,0 +1,20 @@ +package run.halo.app.service; + +import org.springframework.lang.NonNull; + +/** + * Data process service interface. + * + * @author ryanwang + * @date 2019-12-29 + */ +public interface DataProcessService { + + /** + * Replace all url. + * + * @param oldUrl old url must not be null. + * @param newUrl new url must not be null. + */ + void replaceAllUrl(@NonNull String oldUrl, @NonNull String newUrl); +} diff --git a/src/main/java/run/halo/app/service/OptionService.java b/src/main/java/run/halo/app/service/OptionService.java index 7f8763425..450cc924d 100755 --- a/src/main/java/run/halo/app/service/OptionService.java +++ b/src/main/java/run/halo/app/service/OptionService.java @@ -10,6 +10,7 @@ import run.halo.app.exception.MissingPropertyException; import run.halo.app.model.dto.OptionDTO; import run.halo.app.model.dto.OptionSimpleDTO; import run.halo.app.model.entity.Option; +import run.halo.app.model.enums.PostPermalinkType; import run.halo.app.model.enums.ValueEnum; import run.halo.app.model.params.OptionParam; import run.halo.app.model.params.OptionQuery; @@ -338,6 +339,22 @@ public interface OptionService extends CrudService { */ long getBirthday(); + /** + * Get post permalink type. + * + * @return PostPermalinkType + */ + PostPermalinkType getPostPermalinkType(); + + /** + * Replace option url in batch. + * + * @param oldUrl old blog url. + * @param newUrl new blog url. + * @return replaced options. + */ + List replaceUrl(@NonNull String oldUrl, @NonNull String newUrl); + /** * Converts to option output dto. * diff --git a/src/main/java/run/halo/app/service/PhotoService.java b/src/main/java/run/halo/app/service/PhotoService.java index 105fd99e5..2437affbb 100644 --- a/src/main/java/run/halo/app/service/PhotoService.java +++ b/src/main/java/run/halo/app/service/PhotoService.java @@ -80,4 +80,13 @@ public interface PhotoService extends CrudService { * @return list of teams */ List listAllTeams(); + + /** + * Replace photo url in batch. + * + * @param oldUrl old blog url. + * @param newUrl new blog url. + * @return replaced photos. + */ + List replaceUrl(@NonNull String oldUrl, @NonNull String newUrl); } diff --git a/src/main/java/run/halo/app/service/PostCommentService.java b/src/main/java/run/halo/app/service/PostCommentService.java index ca5ac5db0..91bd9665b 100644 --- a/src/main/java/run/halo/app/service/PostCommentService.java +++ b/src/main/java/run/halo/app/service/PostCommentService.java @@ -49,4 +49,9 @@ public interface PostCommentService extends BaseCommentService { @NonNull Page pageTreeBy(@NonNull CommentQuery commentQuery, @NonNull Pageable pageable); + + /** + * Validate CommentBlackList Status + */ + void validateCommentBlackListStatus(); } diff --git a/src/main/java/run/halo/app/service/PostService.java b/src/main/java/run/halo/app/service/PostService.java index dbb978cf2..5217eb19d 100755 --- a/src/main/java/run/halo/app/service/PostService.java +++ b/src/main/java/run/halo/app/service/PostService.java @@ -2,18 +2,16 @@ package run.halo.app.service; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; -import run.halo.app.model.dto.post.BasePostDetailDTO; import run.halo.app.model.entity.Post; import run.halo.app.model.entity.PostMeta; import run.halo.app.model.enums.PostStatus; import run.halo.app.model.params.PostQuery; -import run.halo.app.model.vo.ArchiveMonthVO; -import run.halo.app.model.vo.ArchiveYearVO; -import run.halo.app.model.vo.PostDetailVO; -import run.halo.app.model.vo.PostListVO; +import run.halo.app.model.vo.*; import run.halo.app.service.base.BasePostService; +import javax.validation.constraints.NotNull; import java.util.Collection; import java.util.List; import java.util.Set; @@ -96,6 +94,54 @@ public interface PostService extends BasePostService { @Override Post getBy(@NonNull PostStatus status, @NonNull String url); + /** + * Gets post by post year and month and url. + * + * @param year post create year. + * @param month post create month. + * @param url post url. + * @return post info + */ + @NonNull + Post getBy(@NonNull Integer year, @NonNull Integer month, @NonNull String url); + + /** + * Gets post by post year and month and url. + * + * @param year post create year. + * @param month post create month. + * @param url post url. + * @param status post status. + * @return post info + */ + @NonNull + Post getBy(@NonNull Integer year, @NonNull Integer month, @NonNull String url, @NonNull PostStatus status); + + /** + * Gets post by post year and month and url. + * + * @param year post create year. + * @param month post create month. + * @param day post create day. + * @param url post url. + * @return post info + */ + @NonNull + Post getBy(@NonNull Integer year, @NonNull Integer month, @NonNull Integer day, @NonNull String url); + + /** + * Gets post by post year and month and url. + * + * @param year post create year. + * @param month post create month. + * @param day post create day. + * @param url post url. + * @param status post status. + * @return post info + */ + @NonNull + Post getBy(@NonNull Integer year, @NonNull Integer month, @NonNull Integer day, @NonNull String url, @NonNull PostStatus status); + /** * Removes posts in batch. * @@ -167,14 +213,6 @@ public interface PostService extends BasePostService { @NonNull Page convertToListVo(@NonNull Page postPage); - /** - * Converts to a page of post detail dto. - * - * @param postPage post page must not be null - * @return a page of post detail dto - */ - Page convertToDetailDto(@NonNull Page postPage); - /** * Converts to a page of detail vo. * @@ -189,4 +227,23 @@ public interface PostService extends BasePostService { * @param postId postId must not be null */ void publishVisitEvent(@NonNull Integer postId); + + /** + * Gets pre && next post. + * + * @param currentPost post must not be null + * @return AdjacentPostVO. it contains prePost and nextPost. + * AdjacentPostVO will not be null. But prePost and nextPost may be null. + */ + @NotNull + AdjacentPostVO getAdjacentPosts(Post currentPost); + + /** + * Get Post Pageable default sort + * + * @return + * @Desc contains three parts. First, Top Priority; Second, From Custom index sort; Third, basic id sort + */ + @NotNull + Sort getPostDefaultSort(); } diff --git a/src/main/java/run/halo/app/service/StaticPageService.java b/src/main/java/run/halo/app/service/StaticPageService.java new file mode 100644 index 000000000..72afcafed --- /dev/null +++ b/src/main/java/run/halo/app/service/StaticPageService.java @@ -0,0 +1,53 @@ +package run.halo.app.service; + +import run.halo.app.model.support.StaticPageFile; + +import java.nio.file.Path; +import java.util.List; + +import static run.halo.app.model.support.HaloConst.FILE_SEPARATOR; +import static run.halo.app.model.support.HaloConst.TEMP_DIR; +import static run.halo.app.utils.HaloUtils.ensureSuffix; + +/** + * Static Page service interface. + * + * @author ryanwang + * @date 2019-12-25 + */ +public interface StaticPageService { + + /** + * Static page folder location. + */ + String PAGES_FOLDER = "static_pages"; + + + String STATIC_PAGE_PACK_DIR = ensureSuffix(TEMP_DIR, FILE_SEPARATOR) + "static-pages-pack" + FILE_SEPARATOR; + + String[] USELESS_FILE_SUFFIX = {"ftl", "md", "yaml", "yml", "gitignore"}; + + /** + * Generate pages. + */ + void generate(); + + /** + * Deploy static pages. + */ + void deploy(); + + /** + * Zip static pages directory. + * + * @return zip path + */ + Path zipStaticPagesDirectory(); + + /** + * List file of generated static page. + * + * @return a list of generated static page. + */ + List listFile(); +} diff --git a/src/main/java/run/halo/app/service/ThemeService.java b/src/main/java/run/halo/app/service/ThemeService.java index 01c7e8668..9aaa500da 100644 --- a/src/main/java/run/halo/app/service/ThemeService.java +++ b/src/main/java/run/halo/app/service/ThemeService.java @@ -64,6 +64,11 @@ public interface ThemeService { */ String RENDER_TEMPLATE = "themes/%s/%s"; + /** + * Render template with suffix. + */ + String RENDER_TEMPLATE_SUFFIX = "themes/%s/%s.ftl"; + /** * Theme cache key. */ @@ -231,6 +236,15 @@ public interface ThemeService { @NonNull String render(@NonNull String pageName); + /** + * Renders a theme page. + * + * @param pageName must not be blank + * @return full path of the theme page + */ + @NonNull + String renderWithSuffix(@NonNull String pageName); + /** * Gets current theme id. * diff --git a/src/main/java/run/halo/app/service/ThemeSettingService.java b/src/main/java/run/halo/app/service/ThemeSettingService.java index bec2c5af1..f71c33973 100644 --- a/src/main/java/run/halo/app/service/ThemeSettingService.java +++ b/src/main/java/run/halo/app/service/ThemeSettingService.java @@ -55,4 +55,18 @@ public interface ThemeSettingService { */ @NonNull Map listAsMapBy(@NonNull String themeId); + + /** + * Replace theme setting url in batch. + * + * @param oldUrl old blog url. + * @param newUrl new blog url. + * @return replaced theme settings. + */ + List replaceUrl(@NonNull String oldUrl, @NonNull String newUrl); + + /** + * Delete unused theme setting. + */ + void deleteInactivated(); } diff --git a/src/main/java/run/halo/app/service/base/BaseCommentService.java b/src/main/java/run/halo/app/service/base/BaseCommentService.java index 890304c10..070992f0b 100644 --- a/src/main/java/run/halo/app/service/base/BaseCommentService.java +++ b/src/main/java/run/halo/app/service/base/BaseCommentService.java @@ -304,4 +304,13 @@ public interface BaseCommentService extends CrudSer */ Page filterIpAddress(@NonNull Page commentPage); + /** + * Replace comment url in batch. + * + * @param oldUrl old blog url. + * @param newUrl new blog url. + * @return replaced comments. + */ + List replaceUrl(@NonNull String oldUrl, @NonNull String newUrl); + } diff --git a/src/main/java/run/halo/app/service/base/BasePostService.java b/src/main/java/run/halo/app/service/base/BasePostService.java index e81cda626..3d48b209a 100644 --- a/src/main/java/run/halo/app/service/base/BasePostService.java +++ b/src/main/java/run/halo/app/service/base/BasePostService.java @@ -18,7 +18,8 @@ import java.util.Optional; * Base post service implementation. * * @author johnniang - * @date 19-4-24 + * @author ryanwang + * @date 2019-04-24 */ public interface BasePostService extends CrudService { @@ -63,6 +64,16 @@ public interface BasePostService extends CrudService extends CrudService updateStatusByIds(@NonNull List ids, @NonNull PostStatus status); + + /** + * Replace post blog url in batch. + * + * @param oldUrl old blog url. + * @param newUrl new blog url. + * @return replaced posts. + */ + @NonNull + List replaceUrl(@NonNull String oldUrl, @NonNull String newUrl); } diff --git a/src/main/java/run/halo/app/service/impl/AdminServiceImpl.java b/src/main/java/run/halo/app/service/impl/AdminServiceImpl.java index f541d9fba..b527e4f20 100644 --- a/src/main/java/run/halo/app/service/impl/AdminServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/AdminServiceImpl.java @@ -1,6 +1,5 @@ package run.halo.app.service.impl; -import cn.hutool.core.date.DateUtil; import cn.hutool.core.io.file.FileReader; import cn.hutool.core.lang.Validator; import cn.hutool.core.util.RandomUtil; @@ -38,9 +37,6 @@ import run.halo.app.service.*; import run.halo.app.utils.FileUtils; import run.halo.app.utils.HaloUtils; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; @@ -49,6 +45,8 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -477,6 +475,8 @@ public class AdminServiceImpl implements AdminService { File file = new File(haloProperties.getWorkDir(), LOG_PATH); + List linesArray = new ArrayList<>(); + StringBuilder result = new StringBuilder(); if (!file.exists()) { @@ -497,8 +497,7 @@ public class AdminServiceImpl implements AdminService { randomAccessFile.seek(pos); if (randomAccessFile.readByte() == '\n') { String line = randomAccessFile.readLine(); - result.append(new String(line.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); - result.append(StringUtils.LF); + linesArray.add(new String(line.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); count++; if (count == lines) { break; @@ -507,8 +506,7 @@ public class AdminServiceImpl implements AdminService { } if (pos == 0) { randomAccessFile.seek(0); - result.append(new String(randomAccessFile.readLine().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); - result.append(StringUtils.LF); + linesArray.add(new String(randomAccessFile.readLine().getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); } } } catch (Exception e) { @@ -522,6 +520,14 @@ public class AdminServiceImpl implements AdminService { } } } + + Collections.reverse(linesArray); + + linesArray.forEach(line -> { + result.append(line) + .append(StringUtils.LF); + }); + return result.toString(); } } diff --git a/src/main/java/run/halo/app/service/impl/AttachmentServiceImpl.java b/src/main/java/run/halo/app/service/impl/AttachmentServiceImpl.java index faa6f19f7..1045a86df 100644 --- a/src/main/java/run/halo/app/service/impl/AttachmentServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/AttachmentServiceImpl.java @@ -18,6 +18,7 @@ import run.halo.app.model.entity.Attachment; import run.halo.app.model.enums.AttachmentType; import run.halo.app.model.params.AttachmentQuery; import run.halo.app.model.properties.AttachmentProperties; +import run.halo.app.model.properties.OtherProperties; import run.halo.app.model.support.UploadResult; import run.halo.app.repository.AttachmentRepository; import run.halo.app.service.AttachmentService; @@ -157,13 +158,15 @@ public class AttachmentServiceImpl extends AbstractCrudService replaceUrl(String oldUrl, String newUrl) { + List attachments = listAll(); + List replaced = new ArrayList<>(); + attachments.forEach(attachment -> { + if (StringUtils.isNotEmpty(attachment.getPath())) { + attachment.setPath(attachment.getPath().replaceAll(oldUrl, newUrl)); + } + if (StringUtils.isNotEmpty(attachment.getThumbPath())) { + attachment.setThumbPath(attachment.getThumbPath().replaceAll(oldUrl, newUrl)); + } + replaced.add(attachment); + }); + return updateInBatch(replaced); + } + @Override public Attachment create(Attachment attachment) { Assert.notNull(attachment, "Attachment must not be null"); diff --git a/src/main/java/run/halo/app/service/impl/BaseCommentServiceImpl.java b/src/main/java/run/halo/app/service/impl/BaseCommentServiceImpl.java index 05e86334a..de6193c28 100644 --- a/src/main/java/run/halo/app/service/impl/BaseCommentServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BaseCommentServiceImpl.java @@ -610,6 +610,20 @@ public abstract class BaseCommentServiceImpl extend return commentPage; } + @Override + public List replaceUrl(String oldUrl, String newUrl) { + List comments = listAll(); + List replaced = new ArrayList<>(); + comments.forEach(comment -> { + if (StringUtils.isNotEmpty(comment.getAuthorUrl())) { + comment.setAuthorUrl(comment.getAuthorUrl().replaceAll(oldUrl, newUrl)); + } + replaced.add(comment); + }); + List updated = updateInBatch(replaced); + return convertTo(updated); + } + /** * Get children comments recursively. * diff --git a/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java b/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java index 43436dbaa..a9030af63 100644 --- a/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java @@ -29,10 +29,7 @@ import run.halo.app.utils.HaloUtils; import run.halo.app.utils.MarkdownUtils; import run.halo.app.utils.ServiceUtils; -import java.util.Collections; -import java.util.Date; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -97,6 +94,16 @@ public abstract class BasePostServiceImpl extends Abstrac return postOptional.orElseThrow(() -> new NotFoundException("查询不到该文章的信息").setErrorData(url)); } + @Override + public POST getBy(PostStatus status, Integer id) { + Assert.notNull(status, "Post status must not be null"); + Assert.notNull(id, "Post id must not be null"); + + Optional postOptional = basePostRepository.getByIdAndStatus(id, status); + + return postOptional.orElseThrow(() -> new NotFoundException("查询不到该文章的信息").setErrorData(id)); + } + @Override public List listAllBy(PostStatus status) { Assert.notNull(status, "Post status must not be null"); @@ -395,6 +402,26 @@ public abstract class BasePostServiceImpl extends Abstrac }).collect(Collectors.toList()); } + @Override + public List replaceUrl(String oldUrl, String newUrl) { + List posts = listAll(); + List replaced = new ArrayList<>(); + posts.forEach(post -> { + if (StringUtils.isNotEmpty(post.getThumbnail())) { + post.setThumbnail(post.getThumbnail().replaceAll(oldUrl, newUrl)); + } + if (StringUtils.isNotEmpty(post.getOriginalContent())) { + post.setOriginalContent(post.getOriginalContent().replaceAll(oldUrl, newUrl)); + } + if (StringUtils.isNotEmpty(post.getFormatContent())) { + post.setFormatContent(post.getFormatContent().replaceAll(oldUrl, newUrl)); + } + replaced.add(post); + }); + List updated = updateInBatch(replaced); + return updated.stream().map(this::convertToDetail).collect(Collectors.toList()); + } + @Override public POST create(POST post) { // Check title diff --git a/src/main/java/run/halo/app/service/impl/CommentBlackListServiceImpl.java b/src/main/java/run/halo/app/service/impl/CommentBlackListServiceImpl.java new file mode 100644 index 000000000..51b6c6ac0 --- /dev/null +++ b/src/main/java/run/halo/app/service/impl/CommentBlackListServiceImpl.java @@ -0,0 +1,84 @@ +package run.halo.app.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import run.halo.app.model.entity.CommentBlackList; +import run.halo.app.model.enums.CommentViolationTypeEnum; +import run.halo.app.model.properties.CommentProperties; +import run.halo.app.repository.CommentBlackListRepository; +import run.halo.app.repository.PostCommentRepository; +import run.halo.app.service.CommentBlackListService; +import run.halo.app.service.OptionService; +import run.halo.app.service.base.AbstractCrudService; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.Optional; + +/** + * Comment BlackList Service Implements + * + * @author Lei XinXin + * @date 2020/1/3 + */ + +@Service +@Slf4j +public class CommentBlackListServiceImpl extends AbstractCrudService implements CommentBlackListService { + private static ZoneId zoneId = ZoneId.of("Asia/Shanghai"); + private final CommentBlackListRepository commentBlackListRepository; + private final PostCommentRepository postCommentRepository; + private final OptionService optionService; + + + public CommentBlackListServiceImpl(CommentBlackListRepository commentBlackListRepository, PostCommentRepository postCommentRepository, OptionService optionService) { + super(commentBlackListRepository); + this.commentBlackListRepository = commentBlackListRepository; + this.postCommentRepository = postCommentRepository; + this.optionService = optionService; + } + + @Override + public CommentViolationTypeEnum commentsBanStatus(String ipAddress) { + /* + N=后期可配置 + 1. 获取评论次数; + 2. 判断N分钟内,是否超过规定的次数限制,超过后需要每隔N分钟才能再次评论; + 3. 如果在时隔N分钟内,还有多次评论,可被认定为恶意攻击者; + 4. 对恶意攻击者进行N分钟的封禁; + */ + Optional blackList = commentBlackListRepository.findByIpAddress(ipAddress); + LocalDateTime now = LocalDateTime.now(); + Date endTime = new Date(now.atZone(zoneId).toInstant().toEpochMilli()); + Integer banTime = optionService.getByPropertyOrDefault(CommentProperties.COMMENT_BAN_TIME, Integer.class, 10); + Date startTime = new Date(now.minusMinutes(banTime) + .atZone(zoneId).toInstant().toEpochMilli()); + Integer range = optionService.getByPropertyOrDefault(CommentProperties.COMMENT_RANGE, Integer.class, 30); + boolean isPresent = postCommentRepository.countByIpAndTime(ipAddress, startTime, endTime) >= range; + if (isPresent && blackList.isPresent()) { + update(now, blackList.get(), banTime); + return CommentViolationTypeEnum.FREQUENTLY; + } else if (isPresent) { + CommentBlackList commentBlackList = CommentBlackList + .builder() + .banTime(getBanTime(now, banTime)) + .ipAddress(ipAddress) + .build(); + super.create(commentBlackList); + return CommentViolationTypeEnum.FREQUENTLY; + } + return CommentViolationTypeEnum.NORMAL; + } + + private void update(LocalDateTime localDateTime, CommentBlackList blackList, Integer banTime) { + blackList.setBanTime(getBanTime(localDateTime, banTime)); + int updateResult = commentBlackListRepository.updateByIpAddress(blackList); + Optional.of(updateResult) + .filter(result -> result <= 0).ifPresent(result -> log.error("更新评论封禁时间失败")); + } + + private Date getBanTime(LocalDateTime localDateTime, Integer banTime) { + return new Date(localDateTime.plusMinutes(banTime).atZone(zoneId).toInstant().toEpochMilli()); + } +} diff --git a/src/main/java/run/halo/app/service/impl/DataProcessServiceImpl.java b/src/main/java/run/halo/app/service/impl/DataProcessServiceImpl.java new file mode 100644 index 000000000..84a5a681e --- /dev/null +++ b/src/main/java/run/halo/app/service/impl/DataProcessServiceImpl.java @@ -0,0 +1,65 @@ +package run.halo.app.service.impl; + +import org.springframework.stereotype.Service; +import run.halo.app.service.*; + +/** + * DataProcessService implementation. + * + * @author ryanwang + * @date 2019-12-29 + */ +@Service +public class DataProcessServiceImpl implements DataProcessService { + + private final PostService postService; + + private final SheetService sheetService; + + private final PostCommentService postCommentService; + + private final SheetCommentService sheetCommentService; + + private final JournalCommentService journalCommentService; + + private final AttachmentService attachmentService; + + private final OptionService optionService; + + private final PhotoService photoService; + + private final ThemeSettingService themeSettingService; + + public DataProcessServiceImpl(PostService postService, + SheetService sheetService, + PostCommentService postCommentService, + SheetCommentService sheetCommentService, + JournalCommentService journalCommentService, + AttachmentService attachmentService, + OptionService optionService, + PhotoService photoService, + ThemeSettingService themeSettingService) { + this.postService = postService; + this.sheetService = sheetService; + this.postCommentService = postCommentService; + this.sheetCommentService = sheetCommentService; + this.journalCommentService = journalCommentService; + this.attachmentService = attachmentService; + this.optionService = optionService; + this.photoService = photoService; + this.themeSettingService = themeSettingService; + } + + @Override + public void replaceAllUrl(String oldUrl, String newUrl) { + postService.replaceUrl(oldUrl, newUrl); + sheetService.replaceUrl(oldUrl, newUrl); + postCommentService.replaceUrl(oldUrl, newUrl); + sheetCommentService.replaceUrl(oldUrl, newUrl); + journalCommentService.replaceUrl(oldUrl, newUrl); + attachmentService.replaceUrl(oldUrl, newUrl); + optionService.replaceUrl(oldUrl, newUrl); + photoService.replaceUrl(oldUrl, newUrl); + themeSettingService.replaceUrl(oldUrl, newUrl); + } +} 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 4f5c7a79b..ef3fefaef 100644 --- a/src/main/java/run/halo/app/service/impl/OptionServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/OptionServiceImpl.java @@ -22,6 +22,7 @@ import run.halo.app.exception.MissingPropertyException; import run.halo.app.model.dto.OptionDTO; import run.halo.app.model.dto.OptionSimpleDTO; import run.halo.app.model.entity.Option; +import run.halo.app.model.enums.PostPermalinkType; import run.halo.app.model.enums.ValueEnum; import run.halo.app.model.params.OptionParam; import run.halo.app.model.params.OptionQuery; @@ -36,6 +37,7 @@ import run.halo.app.utils.ValidationUtils; import javax.persistence.criteria.Predicate; import java.util.*; +import java.util.stream.Collectors; /** * OptionService implementation class @@ -460,6 +462,26 @@ public class OptionServiceImpl extends AbstractCrudService impl }); } + @Override + public PostPermalinkType getPostPermalinkType() { + return getEnumByPropertyOrDefault(PermalinkProperties.POST_PERMALINK_TYPE, PostPermalinkType.class, PostPermalinkType.DEFAULT); + } + + @Override + public List replaceUrl(String oldUrl, String newUrl) { + List