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 974e93cf0..1cdf19ba9 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 @@ -51,8 +51,8 @@ public class AdminController { @GetMapping(value = "/is_installed") @ApiOperation("Checks Installation status") public boolean isInstall() { - return optionService - .getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false); + return optionService.getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, + false); } @PostMapping("login/precheck") diff --git a/src/main/java/run/halo/app/controller/admin/api/CategoryController.java b/src/main/java/run/halo/app/controller/admin/api/CategoryController.java index 01dbcdc1b..de41c6d9a 100644 --- a/src/main/java/run/halo/app/controller/admin/api/CategoryController.java +++ b/src/main/java/run/halo/app/controller/admin/api/CategoryController.java @@ -56,10 +56,10 @@ public class CategoryController { @SortDefault(sort = "createTime", direction = DESC) Sort sort, @RequestParam(name = "more", required = false, defaultValue = "false") boolean more) { if (more) { - return postCategoryService.listCategoryWithPostCountDto(sort); + return postCategoryService.listCategoryWithPostCountDto(sort, true); } - return categoryService.convertTo(categoryService.listAll(sort)); + return categoryService.convertTo(categoryService.listAll(sort, true)); } @GetMapping("tree_view") @@ -81,7 +81,8 @@ public class CategoryController { @PutMapping("{categoryId:\\d+}") @ApiOperation("Updates category") public CategoryDTO updateBy(@PathVariable("categoryId") Integer categoryId, - @RequestBody @Valid CategoryParam categoryParam) { + @RequestBody @Valid CategoryParam categoryParam + ) { Category categoryToUpdate = categoryService.getById(categoryId); categoryParam.update(categoryToUpdate); return categoryService.convertTo(categoryService.update(categoryToUpdate)); diff --git a/src/main/java/run/halo/app/controller/admin/api/MigrateController.java b/src/main/java/run/halo/app/controller/admin/api/MigrateController.java index b696d5d46..14761af9e 100644 --- a/src/main/java/run/halo/app/controller/admin/api/MigrateController.java +++ b/src/main/java/run/halo/app/controller/admin/api/MigrateController.java @@ -35,8 +35,8 @@ public class MigrateController { @PostMapping("halo") @ApiOperation("Migrate from Halo") public void migrateHalo(@RequestPart("file") MultipartFile file) { - if (optionService - .getByPropertyOrDefault(PrimaryProperties.IS_INSTALLED, Boolean.class, false)) { + if (optionService.getByPropertyOrDefault( + PrimaryProperties.IS_INSTALLED, Boolean.class, false)) { throw new BadRequestException("无法在博客初始化完成之后迁移数据"); } migrateService.migrate(file, MigrateType.HALO); diff --git a/src/main/java/run/halo/app/controller/admin/api/PostController.java b/src/main/java/run/halo/app/controller/admin/api/PostController.java index 081dc6a27..2e965514d 100644 --- a/src/main/java/run/halo/app/controller/admin/api/PostController.java +++ b/src/main/java/run/halo/app/controller/admin/api/PostController.java @@ -2,13 +2,11 @@ package run.halo.app.controller.admin.api; import static org.springframework.data.domain.Sort.Direction.DESC; -import cn.hutool.core.util.IdUtil; import io.swagger.annotations.ApiOperation; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.List; -import java.util.concurrent.TimeUnit; import javax.validation.Valid; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -22,12 +20,10 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import run.halo.app.cache.AbstractStringCacheStore; import run.halo.app.model.dto.post.BasePostDetailDTO; import run.halo.app.model.dto.post.BasePostMinimalDTO; import run.halo.app.model.dto.post.BasePostSimpleDTO; import run.halo.app.model.entity.Post; -import run.halo.app.model.enums.PostPermalinkType; import run.halo.app.model.enums.PostStatus; import run.halo.app.model.params.PostContentParam; import run.halo.app.model.params.PostParam; @@ -50,15 +46,12 @@ public class PostController { private final PostService postService; - private final AbstractStringCacheStore cacheStore; - private final OptionService optionService; + public PostController(PostService postService, - AbstractStringCacheStore cacheStore, OptionService optionService) { this.postService = postService; - this.cacheStore = cacheStore; this.optionService = optionService; } @@ -70,7 +63,7 @@ public class PostController { @RequestParam(value = "more", defaultValue = "true") Boolean more) { Page postPage = postService.pageBy(postQuery, pageable); if (more) { - return postService.convertToListVo(postPage); + return postService.convertToListVo(postPage, true); } return postService.convertToSimple(postPage); @@ -92,7 +85,7 @@ public class PostController { Page posts = postService.pageBy(status, pageable); if (more) { - return postService.convertToListVo(posts); + return postService.convertToListVo(posts, true); } return postService.convertToSimple(posts); @@ -102,7 +95,7 @@ public class PostController { @ApiOperation("Gets a post") public PostDetailVO getBy(@PathVariable("postId") Integer postId) { Post post = postService.getById(postId); - return postService.convertToDetailVo(post); + return postService.convertToDetailVo(post, true); } @PutMapping("{postId:\\d+}/likes") @@ -114,8 +107,8 @@ public class PostController { @PostMapping @ApiOperation("Creates a post") public PostDetailVO createBy(@Valid @RequestBody PostParam postParam, - @RequestParam(value = "autoSave", required = false, defaultValue = "false") - Boolean autoSave) { + @RequestParam(value = "autoSave", required = false, defaultValue = "false") Boolean autoSave + ) { // Convert to Post post = postParam.convertTo(); return postService.createBy(post, postParam.getTagIds(), postParam.getCategoryIds(), @@ -126,8 +119,8 @@ public class PostController { @ApiOperation("Updates a post") public PostDetailVO updateBy(@Valid @RequestBody PostParam postParam, @PathVariable("postId") Integer postId, - @RequestParam(value = "autoSave", required = false, defaultValue = "false") - Boolean autoSave) { + @RequestParam(value = "autoSave", required = false, defaultValue = "false") Boolean autoSave + ) { // Get the post info Post postToUpdate = postService.getById(postId); @@ -186,11 +179,6 @@ public class PostController { BasePostMinimalDTO postMinimalDTO = postService.convertToMinimal(post); - String token = IdUtil.simpleUUID(); - - // cache preview token - cacheStore.putAny(token, token, 10, TimeUnit.MINUTES); - StringBuilder previewUrl = new StringBuilder(); if (!optionService.isEnabledAbsolutePath()) { @@ -199,14 +187,6 @@ public class PostController { previewUrl.append(postMinimalDTO.getFullPath()); - if (optionService.getPostPermalinkType().equals(PostPermalinkType.ID)) { - previewUrl.append("&token=") - .append(token); - } else { - previewUrl.append("?token=") - .append(token); - } - // build preview post url and return return previewUrl.toString(); } diff --git a/src/main/java/run/halo/app/controller/content/ContentContentController.java b/src/main/java/run/halo/app/controller/content/ContentContentController.java index f33088ed7..9cbef445c 100644 --- a/src/main/java/run/halo/app/controller/content/ContentContentController.java +++ b/src/main/java/run/halo/app/controller/content/ContentContentController.java @@ -1,10 +1,8 @@ package run.halo.app.controller.content; -import cn.hutool.core.util.IdUtil; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; -import java.util.concurrent.TimeUnit; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Controller; @@ -24,12 +22,17 @@ 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.exception.UnsupportedException; import run.halo.app.model.dto.post.BasePostMinimalDTO; +import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Post; import run.halo.app.model.entity.Sheet; +import run.halo.app.model.enums.EncryptTypeEnum; import run.halo.app.model.enums.PostPermalinkType; import run.halo.app.model.enums.PostStatus; import run.halo.app.model.enums.SheetPermalinkType; +import run.halo.app.service.AuthenticationService; +import run.halo.app.service.CategoryService; import run.halo.app.service.OptionService; import run.halo.app.service.PostService; import run.halo.app.service.SheetService; @@ -65,6 +68,10 @@ public class ContentContentController { private final AbstractStringCacheStore cacheStore; + private final AuthenticationService authenticationService; + + private final CategoryService categoryService; + public ContentContentController(PostModel postModel, SheetModel sheetModel, CategoryModel categoryModel, @@ -75,7 +82,9 @@ public class ContentContentController { OptionService optionService, PostService postService, SheetService sheetService, - AbstractStringCacheStore cacheStore) { + AbstractStringCacheStore cacheStore, + AuthenticationService authenticationService, + CategoryService categoryService) { this.postModel = postModel; this.sheetModel = sheetModel; this.categoryModel = categoryModel; @@ -87,6 +96,8 @@ public class ContentContentController { this.postService = postService; this.sheetService = sheetService; this.cacheStore = cacheStore; + this.authenticationService = authenticationService; + this.categoryService = categoryService; } @GetMapping("{prefix}") @@ -225,14 +236,32 @@ public class ContentContentController { throw new NotFoundException("Not Found"); } - @PostMapping(value = "archives/{slug:.*}/password") + @PostMapping(value = "archives/{type}/{slug:.*}/password") @CacheLock(traceRequest = true, expired = 2) - public String password(@PathVariable("slug") String slug, + public String password(@PathVariable("type") String type, + @PathVariable("slug") String slug, @RequestParam(value = "password") String password) throws UnsupportedEncodingException { + + String redirectUrl; + + if (EncryptTypeEnum.POST.getName().equals(type)) { + redirectUrl = doAuthenticationPost(slug, password); + } else if (EncryptTypeEnum.CATEGORY.getName().equals(type)) { + redirectUrl = doAuthenticationCategory(slug, password); + } else { + throw new UnsupportedException("未知的加密类型"); + } + return "redirect:" + redirectUrl; + } + + private String doAuthenticationPost( + String slug, String password) throws UnsupportedEncodingException { Post post = postService.getBy(PostStatus.INTIMATE, slug); post.setSlug(URLEncoder.encode(post.getSlug(), StandardCharsets.UTF_8.name())); + authenticationService.postAuthentication(post, password); + BasePostMinimalDTO postMinimalDTO = postService.convertToMinimal(post); StringBuilder redirectUrl = new StringBuilder(); @@ -243,19 +272,22 @@ public class ContentContentController { redirectUrl.append(postMinimalDTO.getFullPath()); - if (password.equals(post.getPassword())) { - String token = IdUtil.simpleUUID(); - cacheStore.putAny(token, token, 10, TimeUnit.SECONDS); + return redirectUrl.toString(); + } - if (optionService.getPostPermalinkType().equals(PostPermalinkType.ID)) { - redirectUrl.append("&token=") - .append(token); - } else { - redirectUrl.append("?token=") - .append(token); - } + private String doAuthenticationCategory(String slug, String password) { + Category category = categoryService.getBySlugOfNonNull(slug, true); + + authenticationService.categoryAuthentication(category.getId(), password); + + StringBuilder redirectUrl = new StringBuilder(); + + if (!optionService.isEnabledAbsolutePath()) { + redirectUrl.append(optionService.getBlogBaseUrl()); } - return "redirect:" + redirectUrl; + redirectUrl.append(optionService.getCategoriesPrefix()).append(slug); + + return redirectUrl.toString(); } } 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 88a322ca8..f1ef4edf3 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 @@ -2,6 +2,7 @@ package run.halo.app.controller.content.api; import static org.springframework.data.domain.Sort.Direction.DESC; +import com.google.common.collect.Sets; import io.swagger.annotations.ApiOperation; import java.util.List; import org.springframework.data.domain.Page; @@ -14,11 +15,13 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import run.halo.app.exception.ForbiddenException; import run.halo.app.model.dto.CategoryDTO; 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.AuthenticationService; import run.halo.app.service.CategoryService; import run.halo.app.service.PostCategoryService; import run.halo.app.service.PostService; @@ -39,12 +42,16 @@ public class CategoryController { private final PostService postService; + private final AuthenticationService authenticationService; + public CategoryController(CategoryService categoryService, PostCategoryService postCategoryService, - PostService postService) { + PostService postService, + AuthenticationService authenticationService) { this.categoryService = categoryService; this.postCategoryService = postCategoryService; this.postService = postService; + this.authenticationService = authenticationService; } @GetMapping @@ -53,7 +60,7 @@ public class CategoryController { @SortDefault(sort = "updateTime", direction = DESC) Sort sort, @RequestParam(name = "more", required = false, defaultValue = "false") Boolean more) { if (more) { - return postCategoryService.listCategoryWithPostCountDto(sort); + return postCategoryService.listCategoryWithPostCountDto(sort, false); } return categoryService.convertTo(categoryService.listAll(sort)); } @@ -61,13 +68,19 @@ public class CategoryController { @GetMapping("{slug}/posts") @ApiOperation("Lists posts by category slug") public Page listPostsBy(@PathVariable("slug") String slug, + @RequestParam(value = "password", required = false) String password, @PageableDefault(sort = {"topPriority", "updateTime"}, direction = DESC) Pageable pageable) { // Get category by slug - Category category = categoryService.getBySlugOfNonNull(slug); + Category category = categoryService.getBySlugOfNonNull(slug, true); + + if (!authenticationService.categoryAuthentication(category.getId(), password)) { + throw new ForbiddenException("您没有该分类的访问权限"); + } Page postPage = - postCategoryService.pagePostBy(category.getId(), PostStatus.PUBLISHED, pageable); + postCategoryService.pagePostBy(category.getId(), + Sets.immutableEnumSet(PostStatus.PUBLISHED, PostStatus.INTIMATE), pageable); return postService.convertToListVo(postPage); } } 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 index 04bdb18cf..aaab498e8 100644 --- a/src/main/java/run/halo/app/controller/content/model/CategoryModel.java +++ b/src/main/java/run/halo/app/controller/content/model/CategoryModel.java @@ -1,7 +1,10 @@ package run.halo.app.controller.content.model; import static org.springframework.data.domain.Sort.Direction.DESC; +import static run.halo.app.model.support.HaloConst.POST_PASSWORD_TEMPLATE; +import static run.halo.app.model.support.HaloConst.SUFFIX_FTL; +import com.google.common.collect.Sets; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -12,8 +15,10 @@ import org.springframework.ui.Model; import run.halo.app.model.dto.CategoryDTO; import run.halo.app.model.entity.Category; import run.halo.app.model.entity.Post; +import run.halo.app.model.enums.EncryptTypeEnum; import run.halo.app.model.enums.PostStatus; import run.halo.app.model.vo.PostListVO; +import run.halo.app.service.AuthenticationService; import run.halo.app.service.CategoryService; import run.halo.app.service.OptionService; import run.halo.app.service.PostCategoryService; @@ -39,14 +44,20 @@ public class CategoryModel { private final OptionService optionService; - public CategoryModel(CategoryService categoryService, ThemeService themeService, - PostCategoryService postCategoryService, PostService postService, - OptionService optionService) { + private final AuthenticationService authenticationService; + + public CategoryModel(CategoryService categoryService, + ThemeService themeService, + PostCategoryService postCategoryService, + PostService postService, + OptionService optionService, + AuthenticationService authenticationService) { this.categoryService = categoryService; this.themeService = themeService; this.postCategoryService = postCategoryService; this.postService = postService; this.optionService = optionService; + this.authenticationService = authenticationService; } /** @@ -71,15 +82,27 @@ public class CategoryModel { * @return template name */ public String listPost(Model model, String slug, Integer page) { + // Get category by slug - final Category category = categoryService.getBySlugOfNonNull(slug); + final Category category = categoryService.getBySlugOfNonNull(slug, true); + + if (!authenticationService.categoryAuthentication(category.getId(), null)) { + model.addAttribute("slug", category.getSlug()); + model.addAttribute("type", EncryptTypeEnum.CATEGORY.getName()); + if (themeService.templateExists(POST_PASSWORD_TEMPLATE + SUFFIX_FTL)) { + return themeService.render(POST_PASSWORD_TEMPLATE); + } + return "common/template/" + POST_PASSWORD_TEMPLATE; + } + CategoryDTO categoryDTO = categoryService.convertTo(category); final Pageable pageable = PageRequest.of(page - 1, optionService.getArchivesPageSize(), Sort.by(DESC, "topPriority", "createTime")); Page postPage = - postCategoryService.pagePostBy(category.getId(), PostStatus.PUBLISHED, pageable); + postCategoryService.pagePostBy(category.getId(), Sets + .immutableEnumSet(PostStatus.PUBLISHED, PostStatus.INTIMATE), pageable); Page posts = postService.convertToListVo(postPage); // Generate meta description. 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 index b452a7e59..50d37886e 100644 --- a/src/main/java/run/halo/app/controller/content/model/PostModel.java +++ b/src/main/java/run/halo/app/controller/content/model/PostModel.java @@ -13,15 +13,16 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import org.springframework.ui.Model; import run.halo.app.cache.AbstractStringCacheStore; -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.EncryptTypeEnum; import run.halo.app.model.enums.PostEditorType; import run.halo.app.model.enums.PostStatus; import run.halo.app.model.vo.ArchiveYearVO; import run.halo.app.model.vo.PostListVO; +import run.halo.app.service.AuthenticationService; import run.halo.app.service.CategoryService; import run.halo.app.service.OptionService; import run.halo.app.service.PostCategoryService; @@ -59,6 +60,8 @@ public class PostModel { private final AbstractStringCacheStore cacheStore; + private final AuthenticationService authenticationService; + public PostModel(PostService postService, ThemeService themeService, PostCategoryService postCategoryService, @@ -67,7 +70,8 @@ public class PostModel { PostTagService postTagService, TagService tagService, OptionService optionService, - AbstractStringCacheStore cacheStore) { + AbstractStringCacheStore cacheStore, + AuthenticationService authenticationService) { this.postService = postService; this.themeService = themeService; this.postCategoryService = postCategoryService; @@ -77,32 +81,27 @@ public class PostModel { this.tagService = tagService; this.optionService = optionService; this.cacheStore = cacheStore; + this.authenticationService = authenticationService; } public String content(Post post, String token, Model model) { - if (post.getStatus().equals(PostStatus.INTIMATE) && StringUtils.isEmpty(token)) { + if (post.getStatus().equals(PostStatus.INTIMATE) + && !authenticationService.postAuthentication(post, null)) { model.addAttribute("slug", post.getSlug()); + model.addAttribute("type", EncryptTypeEnum.POST.getName()); if (themeService.templateExists(POST_PASSWORD_TEMPLATE + SUFFIX_FTL)) { return themeService.render(POST_PASSWORD_TEMPLATE); } return "common/template/" + POST_PASSWORD_TEMPLATE; } - if (StringUtils.isEmpty(token)) { - post = postService.getBy(PostStatus.PUBLISHED, post.getSlug()); + post = postService.getById(post.getId()); + + if (post.getEditorType().equals(PostEditorType.MARKDOWN)) { + post.setFormatContent(MarkdownUtils.renderHtml(post.getOriginalContent())); } else { - // verify token - String cachedToken = cacheStore.getAny(token, String.class) - .orElseThrow(() -> new ForbiddenException("您没有该文章的访问权限")); - if (!cachedToken.equals(token)) { - throw new ForbiddenException("您没有该文章的访问权限"); - } - if (post.getEditorType().equals(PostEditorType.MARKDOWN)) { - post.setFormatContent(MarkdownUtils.renderHtml(post.getOriginalContent())); - } else { - post.setFormatContent(post.getOriginalContent()); - } + post.setFormatContent(post.getOriginalContent()); } postService.publishVisitEvent(post.getId()); @@ -112,7 +111,7 @@ public class PostModel { postService.getNextPost(post).ifPresent( nextPost -> model.addAttribute("nextPost", postService.convertToDetailVo(nextPost))); - List categories = postCategoryService.listCategoriesBy(post.getId()); + List categories = postCategoryService.listCategoriesBy(post.getId(), false); List tags = postTagService.listTagsBy(post.getId()); List metas = postMetaService.listBy(post.getId()); diff --git a/src/main/java/run/halo/app/core/freemarker/tag/CategoryTagDirective.java b/src/main/java/run/halo/app/core/freemarker/tag/CategoryTagDirective.java index 5ee3da563..447a6e1c6 100644 --- a/src/main/java/run/halo/app/core/freemarker/tag/CategoryTagDirective.java +++ b/src/main/java/run/halo/app/core/freemarker/tag/CategoryTagDirective.java @@ -51,7 +51,7 @@ public class CategoryTagDirective implements TemplateDirectiveModel { switch (method) { case "list": env.setVariable("categories", builder.build().wrap(postCategoryService - .listCategoryWithPostCountDto(Sort.by(DESC, "createTime")))); + .listCategoryWithPostCountDto(Sort.by(DESC, "createTime"), false))); break; case "tree": env.setVariable("categories", builder.build() diff --git a/src/main/java/run/halo/app/core/freemarker/tag/PostTagDirective.java b/src/main/java/run/halo/app/core/freemarker/tag/PostTagDirective.java index 8b875f71b..b27d70e50 100644 --- a/src/main/java/run/halo/app/core/freemarker/tag/PostTagDirective.java +++ b/src/main/java/run/halo/app/core/freemarker/tag/PostTagDirective.java @@ -1,5 +1,6 @@ package run.halo.app.core.freemarker.tag; +import com.google.common.collect.Sets; import freemarker.core.Environment; import freemarker.template.Configuration; import freemarker.template.DefaultObjectWrapperBuilder; @@ -77,12 +78,14 @@ public class PostTagDirective implements TemplateDirectiveModel { case "listByCategoryId": Integer categoryId = Integer.parseInt(params.get("categoryId").toString()); env.setVariable("posts", builder.build().wrap(postService.convertToListVo( - postCategoryService.listPostBy(categoryId, PostStatus.PUBLISHED)))); + postCategoryService.listPostBy(categoryId, + Sets.immutableEnumSet(PostStatus.PUBLISHED, PostStatus.INTIMATE))))); break; case "listByCategorySlug": String categorySlug = params.get("categorySlug").toString(); List posts = - postCategoryService.listPostBy(categorySlug, PostStatus.PUBLISHED); + postCategoryService.listPostBy(categorySlug, + Sets.immutableEnumSet(PostStatus.PUBLISHED, PostStatus.INTIMATE)); env.setVariable("posts", builder.build().wrap(postService.convertToListVo(posts))); break; diff --git a/src/main/java/run/halo/app/exception/UnsupportedException.java b/src/main/java/run/halo/app/exception/UnsupportedException.java new file mode 100644 index 000000000..0495b2001 --- /dev/null +++ b/src/main/java/run/halo/app/exception/UnsupportedException.java @@ -0,0 +1,11 @@ +package run.halo.app.exception; + +/** + * @author ZhiXiang Yuan + * @date 2021/01/18 20:13 + */ +public class UnsupportedException extends ServiceException { + public UnsupportedException(String message) { + super(message); + } +} 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 a77d79489..01c02b986 100644 --- a/src/main/java/run/halo/app/model/dto/CategoryDTO.java +++ b/src/main/java/run/halo/app/model/dto/CategoryDTO.java @@ -31,6 +31,8 @@ public class CategoryDTO implements OutputConverter { private Integer parentId; + private String password; + private Date createTime; private String fullPath; diff --git a/src/main/java/run/halo/app/model/entity/Category.java b/src/main/java/run/halo/app/model/entity/Category.java index 96b56367b..ec6fcc1ec 100644 --- a/src/main/java/run/halo/app/model/entity/Category.java +++ b/src/main/java/run/halo/app/model/entity/Category.java @@ -73,6 +73,12 @@ public class Category extends BaseEntity { @ColumnDefault("0") private Integer parentId; + /** + * Category password. + */ + @Column(name = "password") + private String password; + @Override public void prePersist() { super.prePersist(); diff --git a/src/main/java/run/halo/app/model/enums/EncryptTypeEnum.java b/src/main/java/run/halo/app/model/enums/EncryptTypeEnum.java new file mode 100644 index 000000000..db0bb29b8 --- /dev/null +++ b/src/main/java/run/halo/app/model/enums/EncryptTypeEnum.java @@ -0,0 +1,20 @@ +package run.halo.app.model.enums; + +/** + * @author zhixiang.yuan + * @since 2021/01/24 10:45:33 + */ +public enum EncryptTypeEnum { + + POST("post"), CATEGORY("category"); + + private final String name; + + EncryptTypeEnum(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/run/halo/app/model/params/CategoryParam.java b/src/main/java/run/halo/app/model/params/CategoryParam.java index cc33d764c..c1b02ac74 100644 --- a/src/main/java/run/halo/app/model/params/CategoryParam.java +++ b/src/main/java/run/halo/app/model/params/CategoryParam.java @@ -31,6 +31,9 @@ public class CategoryParam implements InputConverter { @Size(max = 1023, message = "封面图链接的字符长度不能超过 {max}") private String thumbnail; + @Size(max = 255, message = "分类密码的字符长度不能超过 {max}") + private String password; + private Integer parentId = 0; @Override diff --git a/src/main/java/run/halo/app/repository/PostCategoryRepository.java b/src/main/java/run/halo/app/repository/PostCategoryRepository.java index c46738407..6a3c4ab2a 100644 --- a/src/main/java/run/halo/app/repository/PostCategoryRepository.java +++ b/src/main/java/run/halo/app/repository/PostCategoryRepository.java @@ -55,6 +55,19 @@ public interface PostCategoryRepository extends BaseRepository findAllPostIdsByCategoryId(@NonNull Integer categoryId, @NonNull PostStatus status); + /** + * Finds all post ids by category id and post status. + * + * @param categoryId category id must not be null + * @param status post status must not be empty + * @return a set of post id + */ + @NonNull + @Query("select postCategory.postId from PostCategory postCategory, Post post where" + + " postCategory.categoryId = ?1 and post.id = postCategory.postId and post.status in (?2)") + Set findAllPostIdsByCategoryId( + @NonNull Integer categoryId, @NonNull Set status); + /** * Finds all post categories by post id in. * @@ -104,4 +117,14 @@ public interface PostCategoryRepository extends BaseRepository findPostCount(); + + /** + * Finds all post categories by category id list. + * + * @param categoryIdList category id list must not be empty + * @return a list of post category + */ + @Query("select pc from PostCategory pc where pc.categoryId in (?1)") + @NonNull + List findAllByCategoryIdList(List categoryIdList); } diff --git a/src/main/java/run/halo/app/service/AuthenticationService.java b/src/main/java/run/halo/app/service/AuthenticationService.java new file mode 100644 index 000000000..f265640fe --- /dev/null +++ b/src/main/java/run/halo/app/service/AuthenticationService.java @@ -0,0 +1,31 @@ +package run.halo.app.service; + +import run.halo.app.model.entity.Post; + +/** + * Authentication service + * + * @author ZhiXiang Yuan + * @date 2021/01/20 17:39 + */ +public interface AuthenticationService { + + /** + * post authentication + * + * @param post post + * @param password password + * @return authentication success or fail + */ + boolean postAuthentication(Post post, String password); + + /** + * category authentication + * + * @param categoryId category id + * @param password password + * @return authentication success or fail + */ + boolean categoryAuthentication(Integer categoryId, String password); + +} diff --git a/src/main/java/run/halo/app/service/AuthorizationService.java b/src/main/java/run/halo/app/service/AuthorizationService.java new file mode 100644 index 000000000..79d17520b --- /dev/null +++ b/src/main/java/run/halo/app/service/AuthorizationService.java @@ -0,0 +1,65 @@ +package run.halo.app.service; + +import java.util.Set; + +/** + * @author ZhiXiang Yuan + * @date 2021/01/20 17:40 + */ +public interface AuthorizationService { + + /** + * Build post token + * + * @param postId post id + * @return token + */ + static String buildPostToken(Integer postId) { + return "POST:" + postId; + } + + /** + * Build category token + * + * @param categoryId category id + * @return token + */ + static String buildCategoryToken(Integer categoryId) { + return "CATEGORY:" + categoryId; + } + + /** + * Post authorization + * + * @param postId post id + */ + void postAuthorization(Integer postId); + + /** + * CategoryAuthorization + * + * @param categoryId category id + */ + void categoryAuthorization(Integer categoryId); + + /** + * Get access permission store + * + * @return access permission store + */ + Set getAccessPermissionStore(); + + /** + * Delete article authorization + * + * @param postId post id + */ + void deletePostAuthorization(Integer postId); + + /** + * Delete category Authorization + * + * @param categoryId category id + */ + void deleteCategoryAuthorization(Integer categoryId); +} diff --git a/src/main/java/run/halo/app/service/CategoryService.java b/src/main/java/run/halo/app/service/CategoryService.java index 7d1462e3b..08ebd16fe 100755 --- a/src/main/java/run/halo/app/service/CategoryService.java +++ b/src/main/java/run/halo/app/service/CategoryService.java @@ -1,5 +1,6 @@ package run.halo.app.service; +import java.util.Collection; import java.util.List; import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; @@ -35,7 +36,6 @@ public interface CategoryService extends CrudService { * @param slug slug * @return Category */ - @NonNull Category getBySlug(@NonNull String slug); /** @@ -47,6 +47,16 @@ public interface CategoryService extends CrudService { @NonNull Category getBySlugOfNonNull(String slug); + /** + * Get category by slug + * + * @param slug slug + * @param queryEncryptCategory whether to query encryption category + * @return Category + */ + @NonNull + Category getBySlugOfNonNull(String slug, boolean queryEncryptCategory); + /** * Get Category by name. * @@ -64,6 +74,14 @@ public interface CategoryService extends CrudService { @Transactional void removeCategoryAndPostCategoryBy(Integer categoryId); + /** + * Refresh post status, when the post is under the encryption category or is has a password, + * it is changed to private, otherwise it is changed to public. + * + * @param affectedPostIdList affected post id list + */ + void refreshPostStatus(List affectedPostIdList); + /** * List categories by parent id. * @@ -72,6 +90,26 @@ public interface CategoryService extends CrudService { */ List listByParentId(@NonNull Integer id); + /** + * List all category not encrypt. + * + * @param sort sort + * @param queryEncryptCategory whether to query encryption category + * @return list of category. + */ + @NonNull + List listAll(Sort sort, boolean queryEncryptCategory); + + /** + * List all by ids + * + * @param ids ids + * @param queryEncryptCategory whether to query encryption category + * @return List + */ + @NonNull + List listAllByIds(Collection ids, boolean queryEncryptCategory); + /** * Converts to category dto. * @@ -89,4 +127,22 @@ public interface CategoryService extends CrudService { */ @NonNull List convertTo(@Nullable List categories); + + /** + * Filter encrypt category + * + * @param categories this categories is not a category list tree + * @return category list + */ + @NonNull + List filterEncryptCategory(@Nullable List categories); + + /** + * Determine whether the category is encrypted. + * + * @param categoryId category id + * @return whether to encrypt + */ + @NonNull + Boolean categoryHasEncrypt(Integer categoryId); } diff --git a/src/main/java/run/halo/app/service/PostCategoryService.java b/src/main/java/run/halo/app/service/PostCategoryService.java index 34543263f..3869b8822 100644 --- a/src/main/java/run/halo/app/service/PostCategoryService.java +++ b/src/main/java/run/halo/app/service/PostCategoryService.java @@ -35,14 +35,26 @@ public interface PostCategoryService extends CrudService @NonNull List listCategoriesBy(@NonNull Integer postId); + /** + * Lists category by post id. + * + * @param postId post id must not be null + * @param queryEncryptCategory whether to query encryption category + * @return a list of category + */ + @NonNull + List listCategoriesBy(@NonNull Integer postId, @NonNull boolean queryEncryptCategory); + /** * List category list map by post id collection. * * @param postIds post id collection + * @param queryEncryptCategory whether to query encryption category * @return a category list map (key: postId, value: a list of category) */ @NonNull - Map> listCategoryListMap(@Nullable Collection postIds); + Map> listCategoryListMap( + @Nullable Collection postIds, @NonNull boolean queryEncryptCategory); /** * Lists post by category id. @@ -63,6 +75,16 @@ public interface PostCategoryService extends CrudService @NonNull List listPostBy(@NonNull Integer categoryId, @NonNull PostStatus status); + /** + * Lists post by category id and post status. + * + * @param categoryId category id must not be null + * @param status post status + * @return a list of post + */ + @NonNull + List listPostBy(@NonNull Integer categoryId, @NonNull Set status); + /** * Lists post by category slug and post status. * @@ -73,6 +95,16 @@ public interface PostCategoryService extends CrudService @NonNull List listPostBy(@NonNull String slug, @NonNull PostStatus status); + /** + * Lists post by category slug and post status. + * + * @param slug category slug must not be null + * @param status post status + * @return a list of post + */ + @NonNull + List listPostBy(@NonNull String slug, @NonNull Set status); + /** * Pages post by category id. * @@ -95,6 +127,18 @@ public interface PostCategoryService extends CrudService Page pagePostBy(@NonNull Integer categoryId, @NonNull PostStatus status, Pageable pageable); + /** + * Pages post by category id and post status. + * + * @param categoryId category id must not be null + * @param status post status + * @param pageable pageable + * @return page of post + */ + @NonNull + Page pagePostBy( + @NonNull Integer categoryId, @NonNull Set status, Pageable pageable); + /** * Merges or creates post categories by post id and category id set if absent. * @@ -157,8 +201,19 @@ public interface PostCategoryService extends CrudService * Lists category with post count. * * @param sort sort info + * @param queryEncryptCategory whether to query encryption category * @return a list of category dto */ @NonNull - List listCategoryWithPostCountDto(@NonNull Sort sort); + List listCategoryWithPostCountDto( + @NonNull Sort sort, @NonNull boolean queryEncryptCategory); + + /** + * Lists by category id. + * + * @param categoryIdList category id must not be empty + * @return a list of post category + */ + @NonNull + List listByCategoryIdList(@NonNull List categoryIdList); } diff --git a/src/main/java/run/halo/app/service/PostService.java b/src/main/java/run/halo/app/service/PostService.java index ad8117952..7e8283efe 100755 --- a/src/main/java/run/halo/app/service/PostService.java +++ b/src/main/java/run/halo/app/service/PostService.java @@ -248,6 +248,16 @@ public interface PostService extends BasePostService { */ Page convertToDetailVo(@NonNull Page postPage); + /** + * Converts to detail vo. + * + * @param post post must not be null + * @param queryEncryptCategory whether to query encryption category + * @return post detail vo + */ + @NonNull + PostDetailVO convertToDetailVo(@NonNull Post post, @NonNull boolean queryEncryptCategory); + /** * Converts to a page of post list vo. * @@ -257,6 +267,16 @@ public interface PostService extends BasePostService { @NonNull Page convertToListVo(@NonNull Page postPage); + /** + * Converts to a page of post list vo. + * + * @param postPage post page must not be null + * @param queryEncryptCategory whether to query encryption category + * @return a page of post list vo + */ + @NonNull + Page convertToListVo(@NonNull Page postPage, boolean queryEncryptCategory); + /** * Converts to a list of post list vo. * @@ -266,6 +286,14 @@ public interface PostService extends BasePostService { @NonNull List convertToListVo(@NonNull List posts); + /** + * Converts to a list of post list vo. + * + * @param posts post must not be null + * @param queryEncryptCategory whether to query encryption category + * @return a list of post list vo + */ + List convertToListVo(List posts, boolean queryEncryptCategory); /** * Publish a post visit event. diff --git a/src/main/java/run/halo/app/service/impl/AuthenticationServiceImpl.java b/src/main/java/run/halo/app/service/impl/AuthenticationServiceImpl.java new file mode 100644 index 000000000..62fc7a140 --- /dev/null +++ b/src/main/java/run/halo/app/service/impl/AuthenticationServiceImpl.java @@ -0,0 +1,110 @@ +package run.halo.app.service.impl; + +import cn.hutool.core.util.StrUtil; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.springframework.stereotype.Service; +import run.halo.app.model.entity.Category; +import run.halo.app.model.entity.Post; +import run.halo.app.repository.CategoryRepository; +import run.halo.app.repository.PostCategoryRepository; +import run.halo.app.service.AuthenticationService; +import run.halo.app.service.AuthorizationService; + + +/** + * @author ZhiXiang Yuan + * @date 2021/01/20 17:56 + */ +@Service +public class AuthenticationServiceImpl implements AuthenticationService { + + private final CategoryRepository categoryRepository; + + private final AuthorizationService authorizationService; + + private final PostCategoryRepository postCategoryRepository; + + public AuthenticationServiceImpl(PostCategoryRepository postCategoryRepository, + CategoryRepository categoryRepository, + AuthorizationService authorizationService + ) { + this.postCategoryRepository = postCategoryRepository; + this.categoryRepository = categoryRepository; + this.authorizationService = authorizationService; + } + + @Override + public boolean postAuthentication(Post post, String password) { + Set accessPermissionStore = authorizationService.getAccessPermissionStore(); + + if (StrUtil.isNotBlank(post.getPassword())) { + if (accessPermissionStore.contains(AuthorizationService.buildPostToken(post.getId()))) { + return true; + } + + if (post.getPassword().equals(password)) { + authorizationService.postAuthorization(post.getId()); + return true; + } + return false; + } + + Set allCategoryIdSet = postCategoryRepository + .findAllCategoryIdsByPostId(post.getId()); + + if (allCategoryIdSet.isEmpty()) { + return true; + } + + for (Integer categoryId : allCategoryIdSet) { + if (categoryAuthentication(categoryId, password)) { + return true; + } + } + + return false; + } + + @Override + public boolean categoryAuthentication(Integer categoryId, String password) { + + Map idToCategoryMap = categoryRepository.findAll().stream() + .collect(Collectors.toMap(Category::getId, Function.identity())); + + Set accessPermissionStore = authorizationService.getAccessPermissionStore(); + + return doCategoryAuthentication( + idToCategoryMap, accessPermissionStore, categoryId, password); + } + + private boolean doCategoryAuthentication(Map idToCategoryMap, + Set accessPermissionStore, + Integer categoryId, String password) { + + Category category = idToCategoryMap.get(categoryId); + + if (StrUtil.isNotBlank(category.getPassword())) { + if (accessPermissionStore.contains( + AuthorizationService.buildCategoryToken(category.getId()))) { + return true; + } + + if (category.getPassword().equals(password)) { + authorizationService.categoryAuthorization(category.getId()); + return true; + } + + return false; + } + + if (category.getParentId() == 0) { + return true; + } + + return doCategoryAuthentication( + idToCategoryMap, accessPermissionStore, category.getParentId(), password); + } +} diff --git a/src/main/java/run/halo/app/service/impl/AuthorizationServiceImpl.java b/src/main/java/run/halo/app/service/impl/AuthorizationServiceImpl.java new file mode 100644 index 000000000..f372933dc --- /dev/null +++ b/src/main/java/run/halo/app/service/impl/AuthorizationServiceImpl.java @@ -0,0 +1,76 @@ +package run.halo.app.service.impl; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import javax.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Service; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import run.halo.app.cache.AbstractStringCacheStore; +import run.halo.app.service.AuthorizationService; + +/** + * @author ZhiXiang Yuan + * @date 2021/01/21 11:28 + */ +@Service +public class AuthorizationServiceImpl implements AuthorizationService { + + private final AbstractStringCacheStore cacheStore; + + public AuthorizationServiceImpl(AbstractStringCacheStore cacheStore) { + this.cacheStore = cacheStore; + } + + @Override + public void postAuthorization(Integer postId) { + doAuthorization(AuthorizationService.buildPostToken(postId)); + } + + @Override + public void categoryAuthorization(Integer categoryId) { + doAuthorization(AuthorizationService.buildCategoryToken(categoryId)); + } + + @Override + public Set getAccessPermissionStore() { + return cacheStore.getAny(buildAccessPermissionKey(), Set.class).orElse(new HashSet()); + } + + @Override + public void deletePostAuthorization(Integer postId) { + doDeleteAuthorization(AuthorizationService.buildPostToken(postId)); + } + + @Override + public void deleteCategoryAuthorization(Integer categoryId) { + doDeleteAuthorization(AuthorizationService.buildCategoryToken(categoryId)); + } + + private void doDeleteAuthorization(String value) { + Set accessStore = getAccessPermissionStore(); + + accessStore.remove(value); + + cacheStore.putAny(buildAccessPermissionKey(), accessStore, 1, TimeUnit.DAYS); + } + + private void doAuthorization(String value) { + Set accessStore = getAccessPermissionStore(); + + accessStore.add(value); + + cacheStore.putAny(buildAccessPermissionKey(), accessStore, 1, TimeUnit.DAYS); + } + + private String buildAccessPermissionKey() { + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder + .getRequestAttributes(); + + HttpServletRequest request = requestAttributes.getRequest(); + + return "ACCESS_PERMISSION: " + request.getSession().getId(); + } + +} 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 25ea5c996..3cd29980e 100644 --- a/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java @@ -291,11 +291,6 @@ public abstract class BasePostServiceImpl post.setFormatContent(post.getOriginalContent()); } - // if password is not empty,change status to intimate - if (StringUtils.isNotEmpty(post.getPassword()) && post.getStatus() != PostStatus.DRAFT) { - post.setStatus(PostStatus.INTIMATE); - } - // Create or update post if (ServiceUtils.isEmptyId(post.getId())) { // The sheet will be created diff --git a/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java b/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java index 00388db30..020c61430 100644 --- a/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java @@ -2,12 +2,23 @@ package run.halo.app.service.impl; import static run.halo.app.model.support.HaloConst.URL_SEPARATOR; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; import com.google.common.base.Objects; +import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; @@ -16,14 +27,22 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import run.halo.app.exception.AlreadyExistsException; import run.halo.app.exception.NotFoundException; +import run.halo.app.exception.UnsupportedException; import run.halo.app.model.dto.CategoryDTO; import run.halo.app.model.entity.Category; +import run.halo.app.model.entity.Post; +import run.halo.app.model.entity.PostCategory; +import run.halo.app.model.enums.PostStatus; import run.halo.app.model.vo.CategoryVO; import run.halo.app.repository.CategoryRepository; +import run.halo.app.service.AuthenticationService; +import run.halo.app.service.AuthorizationService; import run.halo.app.service.CategoryService; import run.halo.app.service.OptionService; import run.halo.app.service.PostCategoryService; +import run.halo.app.service.PostService; import run.halo.app.service.base.AbstractCrudService; +import run.halo.app.utils.BeanUtils; import run.halo.app.utils.ServiceUtils; /** @@ -44,13 +63,29 @@ public class CategoryServiceImpl extends AbstractCrudService private final OptionService optionService; + private final AuthorizationService authorizationService; + + private PostService postService; + + private AuthenticationService authenticationService; + public CategoryServiceImpl(CategoryRepository categoryRepository, PostCategoryService postCategoryService, - OptionService optionService) { + OptionService optionService, + AuthenticationService authenticationService, + AuthorizationService authorizationService) { super(categoryRepository); this.categoryRepository = categoryRepository; this.postCategoryService = postCategoryService; this.optionService = optionService; + this.authenticationService = authenticationService; + this.authorizationService = authorizationService; + } + + @Lazy + @Autowired + public void setPostService(PostService postService) { + this.postService = postService; } @Override @@ -78,6 +113,10 @@ public class CategoryServiceImpl extends AbstractCrudService } } + if (StrUtil.isNotBlank(category.getPassword())) { + category.setPassword(category.getPassword().trim()); + } + // Create it return super.create(category); } @@ -97,7 +136,7 @@ public class CategoryServiceImpl extends AbstractCrudService CategoryVO topLevelCategory = createTopLevelCategory(); // Concrete the tree - concreteTree(topLevelCategory, categories); + concreteTree(topLevelCategory, categories, false); return topLevelCategory.getChildren(); } @@ -107,8 +146,13 @@ public class CategoryServiceImpl extends AbstractCrudService * * @param parentCategory parent category vo must not be null * @param categories a list of category + * @param fillPassword whether to fill in the password */ - private void concreteTree(CategoryVO parentCategory, List categories) { + private void concreteTree( + CategoryVO parentCategory, + List categories, + Boolean fillPassword + ) { Assert.notNull(parentCategory, "Parent category must not be null"); if (CollectionUtils.isEmpty(categories)) { @@ -142,6 +186,10 @@ public class CategoryServiceImpl extends AbstractCrudService child.setFullPath(fullPath.toString()); + if (!fillPassword) { + child.setPassword(null); + } + // Add child parentCategory.getChildren().add(child); }); @@ -152,7 +200,7 @@ public class CategoryServiceImpl extends AbstractCrudService // Foreach children vos if (!CollectionUtils.isEmpty(parentCategory.getChildren())) { parentCategory.getChildren() - .forEach(childCategory -> concreteTree(childCategory, categories)); + .forEach(childCategory -> concreteTree(childCategory, categories, fillPassword)); } } @@ -174,18 +222,58 @@ public class CategoryServiceImpl extends AbstractCrudService @Override public Category getBySlug(String slug) { - return categoryRepository.getBySlug(slug).orElse(null); + Optional bySlug = categoryRepository.getBySlug(slug); + if (bySlug.isEmpty()) { + return null; + } + + Category category = bySlug.get(); + + if (authenticationService.categoryAuthentication(category.getId(), null)) { + return category; + } + + return null; } @Override public Category getBySlugOfNonNull(String slug) { - return categoryRepository.getBySlug(slug) - .orElseThrow(() -> new NotFoundException("查询不到该分类的信息").setErrorData(slug)); + + Category category = categoryRepository + .getBySlug(slug) + .orElseThrow(() -> new NotFoundException("查询不到该分类的信息").setErrorData(slug)); + + if (authenticationService.categoryAuthentication(category.getId(), null)) { + return category; + } + + throw new NotFoundException("查询不到该分类的信息").setErrorData(slug); + } + + @Override + public Category getBySlugOfNonNull(String slug, boolean queryEncryptCategory) { + if (queryEncryptCategory) { + return categoryRepository.getBySlug(slug) + .orElseThrow(() -> new NotFoundException("查询不到该分类的信息").setErrorData(slug)); + } else { + return this.getBySlugOfNonNull(slug); + } } @Override public Category getByName(String name) { - return categoryRepository.getByName(name).orElse(null); + Optional byName = categoryRepository.getByName(name); + if (byName.isEmpty()) { + return null; + } + + Category category = byName.get(); + + if (authenticationService.categoryAuthentication(category.getId(), null)) { + return category; + } + + return null; } @Override @@ -198,10 +286,44 @@ public class CategoryServiceImpl extends AbstractCrudService update(category); }); } + // Remove category removeById(categoryId); // Remove post categories - postCategoryService.removeByCategoryId(categoryId); + List affectedPostIdList = postCategoryService.removeByCategoryId(categoryId) + .stream().map(PostCategory::getPostId).collect(Collectors.toList()); + + refreshPostStatus(affectedPostIdList); + } + + @Override + public void refreshPostStatus(List affectedPostIdList) { + if (CollectionUtil.isEmpty(affectedPostIdList)) { + return; + } + + for (Integer postId : affectedPostIdList) { + Post post = postService.getById(postId); + + post.setStatus(null); + + if (StrUtil.isNotBlank(post.getPassword())) { + post.setStatus(PostStatus.INTIMATE); + } else { + postCategoryService.listByPostId(postId) + .stream().map(PostCategory::getCategoryId) + .filter(this::categoryHasEncrypt) + .findAny() + .ifPresent(id -> post.setStatus(PostStatus.INTIMATE)); + } + + if (post.getStatus() == null) { + post.setStatus(PostStatus.PUBLISHED); + } + + postService.update(post); + } + } @Override @@ -243,4 +365,302 @@ public class CategoryServiceImpl extends AbstractCrudService .map(this::convertTo) .collect(Collectors.toList()); } + + @Override + public List filterEncryptCategory(List categories) { + if (CollectionUtil.isEmpty(categories)) { + return Collections.emptyList(); + } + + // Concrete the tree + CategoryVO topLevelCategory = createTopLevelCategory(); + + concreteTree(topLevelCategory, categories, true); + + List childrenList = topLevelCategory.getChildren(); + + // filter encrypt category + doFilterEncryptCategory(childrenList); + + List collectorList = new ArrayList<>(); + + collectAllChild(collectorList, childrenList, true); + + for (Category category : collectorList) { + category.setPassword(null); + } + + return collectorList; + } + + /** + * do filter encrypt category + * + * @param categoryList category list + */ + private void doFilterEncryptCategory(List categoryList) { + if (CollectionUtil.isEmpty(categoryList)) { + return; + } + + for (CategoryVO categoryVO : categoryList) { + if (!authenticationService.categoryAuthentication(categoryVO.getId(), null)) { + // if parent category is not certified, the child category is not displayed. + categoryVO.setChildren(null); + } else { + doFilterEncryptCategory(categoryVO.getChildren()); + } + } + } + + /** + * Collect all child from tree + * + * @param collectorList collector + * @param childrenList contains categories of children + * @param doNotCollectEncryptedCategory true is not collect, false is collect + */ + private void collectAllChild(List collectorList, + List childrenList, + Boolean doNotCollectEncryptedCategory) { + if (CollectionUtil.isEmpty(childrenList)) { + return; + } + + for (CategoryVO categoryVO : childrenList) { + + Category category = new Category(); + BeanUtils.updateProperties(categoryVO, category); + + collectorList.add(category); + + if (doNotCollectEncryptedCategory + && !authenticationService.categoryAuthentication(category.getId(), null)) { + continue; + } + + if (CollectionUtil.isNotEmpty(categoryVO.getChildren())) { + collectAllChild(collectorList, + categoryVO.getChildren(), doNotCollectEncryptedCategory); + } + + } + } + + /** + * Collect sub-categories under the specified category. + * + * @param collectorList collector + * @param childrenList contains categories of children + * @param categoryId category id + * @param doNotCollectEncryptedCategory true is not collect, false is collect + */ + private void collectAllChildByCategoryId(List collectorList, + List childrenList, + Integer categoryId, + Boolean doNotCollectEncryptedCategory) { + if (CollectionUtil.isEmpty(childrenList)) { + return; + } + + for (CategoryVO categoryVO : childrenList) { + if (categoryVO.getId().equals(categoryId)) { + collectAllChild(collectorList, + categoryVO.getChildren(), doNotCollectEncryptedCategory); + break; + } + } + } + + @Override + public List listAll(Sort sort, boolean queryEncryptCategory) { + if (queryEncryptCategory) { + return super.listAll(sort); + } else { + return this.listAll(sort); + } + } + + @Override + public List listAll() { + return filterEncryptCategory(super.listAll()); + } + + @Override + public List listAll(Sort sort) { + return filterEncryptCategory(super.listAll(sort)); + } + + @Override + public Page listAll(Pageable pageable) { + // To prevent developers from querying encrypted categories, + // so paging query operations are not supported here. If you + // really need to use this method, refactor this method to do memory paging. + throw new UnsupportedException("Does not support business layer paging query."); + } + + @Override + public List listAllByIds(Collection integers, boolean queryEncryptCategory) { + if (queryEncryptCategory) { + return super.listAllByIds(integers); + } else { + return this.listAllByIds(integers); + } + } + + @Override + public List listAllByIds(Collection integers) { + return filterEncryptCategory(super.listAllByIds(integers)); + } + + @Override + public List listAllByIds(Collection integers, Sort sort) { + return filterEncryptCategory(super.listAllByIds(integers, sort)); + } + + @Override + @Transactional + public Category update(Category category) { + Category update = super.update(category); + + if (StrUtil.isNotBlank(category.getPassword())) { + doEncryptPost(category); + } else { + doDecryptPost(category); + } + + // Remove authorization every time an category is updated. + authorizationService.deleteCategoryAuthorization(category.getId()); + + return update; + } + + /** + * Encrypting a category requires encrypting all articles under the category + * + * @param category need encrypt category + */ + private void doEncryptPost(Category category) { + CategoryVO topLevelCategory = createTopLevelCategory(); + + concreteTree(topLevelCategory, super.listAll(), true); + + List collectorList = new ArrayList<>(); + + collectAllChildByCategoryId(collectorList, + topLevelCategory.getChildren(), category.getId(), true); + + Optional.of(collectorList.stream().map(Category::getId).collect(Collectors.toList())) + .map(categoryIdList -> { + categoryIdList.add(category.getId()); + return categoryIdList; + }) + .map(postCategoryService::listByCategoryIdList) + + .filter(postCategoryList -> !postCategoryList.isEmpty()) + .map(postCategoryList -> postCategoryList + .stream().map(PostCategory::getPostId).distinct().collect(Collectors.toList())) + + .filter(postIdList -> !postIdList.isEmpty()) + .map(postIdList -> postService.listAllByIds(postIdList)) + + .filter(postList -> !postList.isEmpty()) + .map(postList -> postList.stream() + .filter(post -> PostStatus.PUBLISHED.equals(post.getStatus())) + .map(Post::getId).collect(Collectors.toList())) + + .filter(postIdList -> !postIdList.isEmpty()) + .map(postIdList -> postService.updateStatusByIds(postIdList, PostStatus.INTIMATE)); + } + + private void doDecryptPost(Category category) { + + List allCategoryList = super.listAll(); + + Map idToCategoryMap = allCategoryList.stream().collect( + Collectors.toMap(Category::getId, Function.identity())); + + if (doCategoryHasEncrypt(idToCategoryMap, category.getParentId())) { + // If the parent category is encrypted, there is no need to update the encryption status + return; + } + + CategoryVO topLevelCategory = createTopLevelCategory(); + + concreteTree(topLevelCategory, allCategoryList, true); + + List collectorList = new ArrayList<>(); + + // Only collect unencrypted sub-categories under the category. + collectAllChildByCategoryId(collectorList, + topLevelCategory.getChildren(), category.getId(), false); + // Collect the currently decrypted category + collectorList.add(category); + + Optional.of(collectorList.stream().map(Category::getId).collect(Collectors.toList())) + .map(postCategoryService::listByCategoryIdList) + + .filter(postCategoryList -> !postCategoryList.isEmpty()) + .map(postCategoryList -> postCategoryList + .stream().map(PostCategory::getPostId).distinct().collect(Collectors.toList())) + + .filter(postIdList -> !postIdList.isEmpty()) + .map(postIdList -> postService.listAllByIds(postIdList)) + + .filter(postList -> !postList.isEmpty()) + .map(postList -> postList.stream() + .filter(post -> StrUtil.isBlank(post.getPassword())) + .filter(post -> PostStatus.INTIMATE.equals(post.getStatus())) + .map(Post::getId).collect(Collectors.toList())) + + .filter(postIdList -> !postIdList.isEmpty()) + .map(postIdList -> postService.updateStatusByIds(postIdList, PostStatus.PUBLISHED)); + } + + @Override + public Boolean categoryHasEncrypt(Integer categoryId) { + List allCategoryList = super.listAll(); + + Map idToCategoryMap = allCategoryList.stream().collect( + Collectors.toMap(Category::getId, Function.identity())); + + return doCategoryHasEncrypt(idToCategoryMap, categoryId); + } + + /** + * Find whether the parent category is encrypted. + * + * @param idToCategoryMap find category by id + * @param categoryId category id + * @return whether to encrypt + */ + private boolean doCategoryHasEncrypt( + Map idToCategoryMap, Integer categoryId) { + + if (categoryId == 0) { + return false; + } + + Category category = idToCategoryMap.get(categoryId); + + if (StrUtil.isNotBlank(category.getPassword())) { + return true; + } + + return doCategoryHasEncrypt(idToCategoryMap, category.getParentId()); + } + + + @Override + public List updateInBatch(Collection categories) { + if (CollectionUtils.isEmpty(categories)) { + return Collections.emptyList(); + } + + ArrayList resultList = new ArrayList<>(); + for (Category category : categories) { + resultList.add(update(category)); + } + return resultList; + } } diff --git a/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java b/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java index a62574533..8fc74383f 100644 --- a/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java @@ -8,8 +8,11 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -23,9 +26,9 @@ import run.halo.app.model.entity.Post; import run.halo.app.model.entity.PostCategory; import run.halo.app.model.enums.PostStatus; import run.halo.app.model.projection.CategoryPostCountProjection; -import run.halo.app.repository.CategoryRepository; import run.halo.app.repository.PostCategoryRepository; import run.halo.app.repository.PostRepository; +import run.halo.app.service.CategoryService; import run.halo.app.service.OptionService; import run.halo.app.service.PostCategoryService; import run.halo.app.service.base.AbstractCrudService; @@ -47,33 +50,44 @@ public class PostCategoryServiceImpl extends AbstractCrudService listCategoriesBy(Integer postId) { + return listCategoriesBy(postId, false); + } + + @Override + public List listCategoriesBy(Integer postId, boolean queryEncryptCategory) { Assert.notNull(postId, "Post id must not be null"); // Find all category ids Set categoryIds = postCategoryRepository.findAllCategoryIdsByPostId(postId); - return categoryRepository.findAllById(categoryIds); + return categoryService.listAllByIds(categoryIds, queryEncryptCategory); } + @Override - public Map> listCategoryListMap(Collection postIds) { + public Map> listCategoryListMap( + Collection postIds, boolean queryEncryptCategory) { if (CollectionUtils.isEmpty(postIds)) { return Collections.emptyMap(); } @@ -86,7 +100,7 @@ public class PostCategoryServiceImpl extends AbstractCrudService categories = categoryRepository.findAllById(categoryIds); + List categories = categoryService.listAllByIds(categoryIds, queryEncryptCategory); // Convert to category map Map categoryMap = ServiceUtils.convertToMap(categories, Category::getId); @@ -124,13 +138,45 @@ public class PostCategoryServiceImpl extends AbstractCrudService listPostBy(Integer categoryId, Set status) { + Assert.notNull(categoryId, "Category id must not be null"); + Assert.notNull(status, "Post status must not be null"); + + // Find all post ids + Set postIds = postCategoryRepository + .findAllPostIdsByCategoryId(categoryId, status); + + return postRepository.findAllById(postIds); + } + + @Override + public List listPostBy(String slug, Set status) { + Assert.notNull(slug, "Category slug must not be null"); + Assert.notNull(status, "Post status must not be null"); + + Category category = categoryService.getBySlug(slug); + + if (Objects.isNull(category)) { + throw new NotFoundException("查询不到该分类的信息").setErrorData(slug); + } + + Set postsIds = postCategoryRepository + .findAllPostIdsByCategoryId(category.getId(), status); + + return postRepository.findAllById(postsIds); + } + @Override public List listPostBy(String slug, PostStatus status) { Assert.notNull(slug, "Category slug must not be null"); Assert.notNull(status, "Post status must not be null"); - Category category = categoryRepository.getBySlug(slug) - .orElseThrow(() -> new NotFoundException("查询不到该分类的信息").setErrorData(slug)); + Category category = categoryService.getBySlug(slug); + + if (Objects.isNull(category)) { + throw new NotFoundException("查询不到该分类的信息").setErrorData(slug); + } Set postsIds = postCategoryRepository.findAllPostIdsByCategoryId(category.getId(), status); @@ -155,6 +201,19 @@ public class PostCategoryServiceImpl extends AbstractCrudService postIds = postCategoryRepository + .findAllPostIdsByCategoryId(categoryId, status); + + return postRepository.findAllByIdIn(postIds, pageable); + } + + @Override + public Page pagePostBy(Integer categoryId, Set status, Pageable pageable) { + Assert.notNull(categoryId, "Category id must not be null"); + Assert.notNull(status, "Post status must not be null"); + Assert.notNull(pageable, "Page info must not be null"); + // Find all post ids Set postIds = postCategoryRepository.findAllPostIdsByCategoryId(categoryId, status); @@ -245,10 +304,10 @@ public class PostCategoryServiceImpl extends AbstractCrudService listCategoryWithPostCountDto(Sort sort) { + public List listCategoryWithPostCountDto( + Sort sort, boolean queryEncryptCategory) { Assert.notNull(sort, "Sort info must not be null"); - - List categories = categoryRepository.findAll(sort); + List categories = categoryService.listAll(sort, queryEncryptCategory); // Query category post count Map categoryPostCountMap = ServiceUtils @@ -284,4 +343,10 @@ public class PostCategoryServiceImpl extends AbstractCrudService listByCategoryIdList(List categoryIdList) { + Assert.notEmpty(categoryIdList, "category id list not empty"); + return postCategoryRepository.findAllByCategoryIdList(categoryIdList); + } } diff --git a/src/main/java/run/halo/app/service/impl/PostServiceImpl.java b/src/main/java/run/halo/app/service/impl/PostServiceImpl.java index 78a9b233f..ba5a7143c 100644 --- a/src/main/java/run/halo/app/service/impl/PostServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/PostServiceImpl.java @@ -3,6 +3,7 @@ package run.halo.app.service.impl; import static org.springframework.data.domain.Sort.Direction.DESC; import static run.halo.app.model.support.HaloConst.URL_SEPARATOR; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import java.util.ArrayList; @@ -60,6 +61,7 @@ import run.halo.app.model.vo.PostListVO; import run.halo.app.model.vo.PostMarkdownVO; import run.halo.app.repository.PostRepository; import run.halo.app.repository.base.BasePostRepository; +import run.halo.app.service.AuthorizationService; import run.halo.app.service.CategoryService; import run.halo.app.service.OptionService; import run.halo.app.service.PostCategoryService; @@ -106,6 +108,8 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe private final OptionService optionService; + private final AuthorizationService authorizationService; + public PostServiceImpl(BasePostRepository basePostRepository, OptionService optionService, PostRepository postRepository, @@ -115,7 +119,8 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe PostCategoryService postCategoryService, PostCommentService postCommentService, ApplicationEventPublisher eventPublisher, - PostMetaService postMetaService) { + PostMetaService postMetaService, + AuthorizationService authorizationService) { super(basePostRepository, optionService); this.postRepository = postRepository; this.tagService = tagService; @@ -126,6 +131,7 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe this.eventPublisher = eventPublisher; this.postMetaService = postMetaService; this.optionService = optionService; + this.authorizationService = authorizationService; } @Override @@ -501,10 +507,16 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe @Override public PostDetailVO convertToDetailVo(Post post) { + return convertToDetailVo(post, false); + } + + @Override + public PostDetailVO convertToDetailVo(Post post, boolean queryEncryptCategory) { // List tags List tags = postTagService.listTagsBy(post.getId()); // List categories - List categories = postCategoryService.listCategoriesBy(post.getId()); + List categories = postCategoryService + .listCategoriesBy(post.getId(), queryEncryptCategory); // List metas List metas = postMetaService.listBy(post.getId()); // Convert to detail vo @@ -552,6 +564,11 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe @Override public Page convertToListVo(Page postPage) { + return convertToListVo(postPage, false); + } + + @Override + public Page convertToListVo(Page postPage, boolean queryEncryptCategory) { Assert.notNull(postPage, "Post page must not be null"); List posts = postPage.getContent(); @@ -563,7 +580,7 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe // Get category list map Map> categoryListMap = postCategoryService - .listCategoryListMap(postIds); + .listCategoryListMap(postIds, queryEncryptCategory); // Get comment count Map commentCountMap = postCommentService.countByPostIds(postIds); @@ -612,6 +629,11 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe @Override public List convertToListVo(List posts) { + return convertToListVo(posts, false); + } + + @Override + public List convertToListVo(List posts, boolean queryEncryptCategory) { Assert.notNull(posts, "Post page must not be null"); Set postIds = ServiceUtils.fetchProperty(posts, Post::getId); @@ -621,7 +643,7 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe // Get category list map Map> categoryListMap = postCategoryService - .listCategoryListMap(postIds); + .listCategoryListMap(postIds, queryEncryptCategory); // Get comment count Map commentCountMap = postCommentService.countByPostIds(postIds); @@ -800,6 +822,24 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe Assert.notNull(post, "Post param must not be null"); // Create or update post + Boolean needEncrypt = Optional.ofNullable(categoryIds) + .filter(CollectionUtil::isNotEmpty) + .map(categoryIdSet -> { + for (Integer categoryId : categoryIdSet) { + if (categoryService.categoryHasEncrypt(categoryId)) { + return true; + } + } + return false; + }).orElse(Boolean.FALSE); + + // if password is not empty or parent category has encrypt, change status to intimate + if (post.getStatus() != PostStatus.DRAFT + && (StringUtils.isNotEmpty(post.getPassword()) || needEncrypt) + ) { + post.setStatus(PostStatus.INTIMATE); + } + post = super.createOrUpdateBy(post); postTagService.removeByPostId(post.getId()); @@ -810,7 +850,7 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe List tags = tagService.listAllByIds(tagIds); // List all categories - List categories = categoryService.listAllByIds(categoryIds); + List categories = categoryService.listAllByIds(categoryIds, true); // Create post tags List postTags = postTagService.mergeOrCreateByIfAbsent(post.getId(), @@ -830,10 +870,34 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe .createOrUpdateByPostId(post.getId(), metas); log.debug("Created post metas: [{}]", postMetaList); + // Remove authorization every time an post is created or updated. + authorizationService.deletePostAuthorization(post.getId()); + // Convert to post detail vo return convertTo(post, tags, categories, postMetaList); } + @Override + @Transactional + public Post updateStatus(PostStatus status, Integer postId) { + super.updateStatus(status, postId); + if (PostStatus.PUBLISHED.equals(status)) { + // When the update status is published, it is necessary to determine whether + // the post status should be converted to a intimate post + categoryService.refreshPostStatus(Collections.singletonList(postId)); + } + return getById(postId); + } + + @Override + @Transactional + public List updateStatusByIds(List ids, PostStatus status) { + if (CollectionUtils.isEmpty(ids)) { + return Collections.emptyList(); + } + return ids.stream().map(id -> updateStatus(status, id)).collect(Collectors.toList()); + } + @Override public void publishVisitEvent(Integer postId) { eventPublisher.publishEvent(new PostVisitEvent(this, postId)); diff --git a/src/main/resources/templates/common/template/post_password.ftl b/src/main/resources/templates/common/template/post_password.ftl index 1807440ff..17d89f334 100644 --- a/src/main/resources/templates/common/template/post_password.ftl +++ b/src/main/resources/templates/common/template/post_password.ftl @@ -152,9 +152,9 @@
-
+
- +