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 b4a6864dd9..bc4fe8c336 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 @@ -57,10 +57,10 @@ public List listAll( @SortDefault(sort = "priority", direction = ASC) Sort sort, @RequestParam(name = "more", required = false, defaultValue = "false") boolean more) { if (more) { - return postCategoryService.listCategoryWithPostCountDto(sort, true); + return postCategoryService.listCategoryWithPostCountDto(sort); } - return categoryService.convertTo(categoryService.listAll(sort, true)); + return categoryService.convertTo(categoryService.listAll(sort)); } @GetMapping("tree_view") 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 296069ea7e..985a43d778 100644 --- a/src/main/java/run/halo/app/controller/content/ContentContentController.java +++ b/src/main/java/run/halo/app/controller/content/ContentContentController.java @@ -1,8 +1,9 @@ package run.halo.app.controller.content; +import static run.halo.app.model.support.HaloConst.POST_PASSWORD_TEMPLATE; +import static run.halo.app.model.support.HaloConst.SUFFIX_FTL; + import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import javax.servlet.http.HttpServletRequest; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -16,6 +17,8 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import run.halo.app.cache.lock.CacheLock; +import run.halo.app.controller.content.auth.ContentAuthenticationManager; +import run.halo.app.controller.content.auth.ContentAuthenticationRequest; import run.halo.app.controller.content.model.CategoryModel; import run.halo.app.controller.content.model.JournalModel; import run.halo.app.controller.content.model.LinkModel; @@ -23,24 +26,27 @@ 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.AuthenticationException; import run.halo.app.exception.NotFoundException; import run.halo.app.exception.UnsupportedException; import run.halo.app.model.dto.CategoryDTO; 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; +import run.halo.app.service.ThemeService; /** * @author ryanwang + * @author guqing * @date 2020-01-07 */ @Slf4j @@ -68,10 +74,12 @@ public class ContentContentController { private final SheetService sheetService; - private final AuthenticationService authenticationService; - private final CategoryService categoryService; + private final ThemeService themeService; + + private final ContentAuthenticationManager providerManager; + public ContentContentController(PostModel postModel, SheetModel sheetModel, CategoryModel categoryModel, @@ -82,8 +90,9 @@ public ContentContentController(PostModel postModel, OptionService optionService, PostService postService, SheetService sheetService, - AuthenticationService authenticationService, - CategoryService categoryService) { + CategoryService categoryService, + ThemeService themeService, + ContentAuthenticationManager providerManager) { this.postModel = postModel; this.sheetModel = sheetModel; this.categoryModel = categoryModel; @@ -94,8 +103,9 @@ public ContentContentController(PostModel postModel, this.optionService = optionService; this.postService = postService; this.sheetService = sheetService; - this.authenticationService = authenticationService; this.categoryService = categoryService; + this.themeService = themeService; + this.providerManager = providerManager; } @GetMapping("{prefix}") @@ -240,18 +250,60 @@ public String content(@PathVariable("year") Integer year, @CacheLock(traceRequest = true, expired = 2) public String password(@PathVariable("type") String type, @PathVariable("slug") String slug, - @RequestParam(value = "password") String password) throws UnsupportedEncodingException { - - String redirectUrl; - + @RequestParam(value = "password") String password, + HttpServletRequest request) throws UnsupportedEncodingException { if (EncryptTypeEnum.POST.getName().equals(type)) { - redirectUrl = doAuthenticationPost(slug, password); + return authenticatePost(slug, type, password, request); } else if (EncryptTypeEnum.CATEGORY.getName().equals(type)) { - redirectUrl = doAuthenticationCategory(slug, password); + return authenticateCategory(slug, type, password, request); } else { throw new UnsupportedException("未知的加密类型"); } - return "redirect:" + redirectUrl; + } + + private String authenticatePost(String slug, String type, String password, + HttpServletRequest request) { + ContentAuthenticationRequest authRequest = new ContentAuthenticationRequest(); + authRequest.setPassword(password); + Post post = postService.getBy(PostStatus.INTIMATE, slug); + authRequest.setId(post.getId()); + authRequest.setPrincipal(EncryptTypeEnum.POST.getName()); + try { + providerManager.authenticate(authRequest); + BasePostMinimalDTO basePostMinimal = postService.convertToMinimal(post); + return "redirect:" + buildRedirectUrl(basePostMinimal.getFullPath()); + } catch (AuthenticationException e) { + request.setAttribute("errorMsg", e.getMessage()); + request.setAttribute("type", type); + request.setAttribute("slug", slug); + return getPasswordPageUriToForward(); + } + } + + private String authenticateCategory(String slug, String type, String password, + HttpServletRequest request) { + ContentAuthenticationRequest authRequest = new ContentAuthenticationRequest(); + authRequest.setPassword(password); + Category category = categoryService.getBySlugOfNonNull(slug); + authRequest.setId(category.getId()); + authRequest.setPrincipal(EncryptTypeEnum.CATEGORY.getName()); + try { + providerManager.authenticate(authRequest); + CategoryDTO categoryDto = categoryService.convertTo(category); + return "redirect:" + buildRedirectUrl(categoryDto.getFullPath()); + } catch (AuthenticationException e) { + request.setAttribute("errorMsg", e.getMessage()); + request.setAttribute("type", type); + request.setAttribute("slug", slug); + return getPasswordPageUriToForward(); + } + } + + private String getPasswordPageUriToForward() { + if (themeService.templateExists(POST_PASSWORD_TEMPLATE + SUFFIX_FTL)) { + return themeService.render(POST_PASSWORD_TEMPLATE); + } + return "common/template/" + POST_PASSWORD_TEMPLATE; } private NotFoundException buildPathNotFoundException() { @@ -265,41 +317,13 @@ private NotFoundException buildPathNotFoundException() { return new NotFoundException("无法定位到该路径:" + requestUri); } - 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); - + private String buildRedirectUrl(String fullPath) { StringBuilder redirectUrl = new StringBuilder(); if (!optionService.isEnabledAbsolutePath()) { redirectUrl.append(optionService.getBlogBaseUrl()); } - - redirectUrl.append(postMinimalDTO.getFullPath()); - - return redirectUrl.toString(); - } - - private String doAuthenticationCategory(String slug, String password) { - CategoryDTO - category = categoryService.convertTo(categoryService.getBySlugOfNonNull(slug, true)); - - authenticationService.categoryAuthentication(category.getId(), password); - - StringBuilder redirectUrl = new StringBuilder(); - - if (!optionService.isEnabledAbsolutePath()) { - redirectUrl.append(optionService.getBlogBaseUrl()); - } - - redirectUrl.append(category.getFullPath()); - + redirectUrl.append(fullPath); 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 fbbf4ba3e9..8eeda9d3f0 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 @@ -5,23 +5,28 @@ import com.google.common.collect.Sets; import io.swagger.annotations.ApiOperation; import java.util.List; +import java.util.Set; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.data.web.SortDefault; +import org.springframework.util.Assert; 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 org.springframework.web.bind.annotation.RestController; +import run.halo.app.controller.content.auth.CategoryAuthentication; +import run.halo.app.controller.content.auth.ContentAuthenticationManager; +import run.halo.app.controller.content.auth.ContentAuthenticationRequest; 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.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.PostCategoryService; import run.halo.app.service.PostService; @@ -42,16 +47,20 @@ public class CategoryController { private final PostService postService; - private final AuthenticationService authenticationService; + private final CategoryAuthentication categoryAuthentication; + + private final ContentAuthenticationManager contentAuthenticationManager; public CategoryController(CategoryService categoryService, PostCategoryService postCategoryService, PostService postService, - AuthenticationService authenticationService) { + CategoryAuthentication categoryAuthentication, + ContentAuthenticationManager contentAuthenticationManager) { this.categoryService = categoryService; this.postCategoryService = postCategoryService; this.postService = postService; - this.authenticationService = authenticationService; + this.categoryAuthentication = categoryAuthentication; + this.contentAuthenticationManager = contentAuthenticationManager; } @GetMapping @@ -60,7 +69,7 @@ public List listCategories( @SortDefault(sort = "updateTime", direction = DESC) Sort sort, @RequestParam(name = "more", required = false, defaultValue = "false") Boolean more) { if (more) { - return postCategoryService.listCategoryWithPostCountDto(sort, false); + return postCategoryService.listCategoryWithPostCountDto(sort); } return categoryService.convertTo(categoryService.listAll(sort)); } @@ -72,15 +81,37 @@ public Page listPostsBy(@PathVariable("slug") String slug, @PageableDefault(sort = {"topPriority", "updateTime"}, direction = DESC) Pageable pageable) { // Get category by slug - Category category = categoryService.getBySlugOfNonNull(slug, true); + Category category = categoryService.getBySlugOfNonNull(slug); - if (!authenticationService.categoryAuthentication(category.getId(), password)) { - throw new ForbiddenException("您没有该分类的访问权限"); + Set statusesToQuery = Sets.immutableEnumSet(PostStatus.PUBLISHED); + if (allowIntimatePosts(category.getId(), password)) { + statusesToQuery = Sets.immutableEnumSet(PostStatus.PUBLISHED, PostStatus.INTIMATE); } Page postPage = - postCategoryService.pagePostBy(category.getId(), - Sets.immutableEnumSet(PostStatus.PUBLISHED), pageable); + postCategoryService.pagePostBy(category.getId(), statusesToQuery, pageable); return postService.convertToListVo(postPage); } + + private boolean allowIntimatePosts(Integer categoryId, String password) { + Assert.notNull(categoryId, "The categoryId must not be null."); + + if (!categoryService.isPrivate(categoryId)) { + return false; + } + + if (categoryAuthentication.isAuthenticated(categoryId)) { + return true; + } + + if (password != null) { + ContentAuthenticationRequest authRequest = + ContentAuthenticationRequest.of(categoryId, password, + EncryptTypeEnum.CATEGORY.getName()); + // authenticate this request,throw an error if authenticate failed + contentAuthenticationManager.authenticate(authRequest); + return true; + } + throw new ForbiddenException("您没有该分类的访问权限"); + } } diff --git a/src/main/java/run/halo/app/controller/content/auth/CategoryAuthentication.java b/src/main/java/run/halo/app/controller/content/auth/CategoryAuthentication.java new file mode 100644 index 0000000000..ca4a1c3452 --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/auth/CategoryAuthentication.java @@ -0,0 +1,85 @@ +package run.halo.app.controller.content.auth; + +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import run.halo.app.cache.AbstractStringCacheStore; +import run.halo.app.model.entity.Category; +import run.halo.app.model.enums.EncryptTypeEnum; +import run.halo.app.service.CategoryService; + +/** + * Authentication for category. + * + * @author guqing + * @date 2022-02-23 + */ +@Component +public class CategoryAuthentication implements ContentAuthentication { + + private final CategoryService categoryService; + + private final AbstractStringCacheStore cacheStore; + + public CategoryAuthentication(CategoryService categoryService, + AbstractStringCacheStore cacheStore) { + this.categoryService = categoryService; + this.cacheStore = cacheStore; + } + + @Override + @NonNull + public Object getPrincipal() { + return EncryptTypeEnum.CATEGORY.getName(); + } + + @Override + public boolean isAuthenticated(Integer categoryId) { + Category category = categoryService.getById(categoryId); + if (category.getPassword() == null) { + // All parent category is not encrypted + if (categoryService.lookupFirstEncryptedBy(category.getId()).isEmpty()) { + return true; + } + } + + String sessionId = getSessionId(); + // No session is represent a client request + if (StringUtils.isEmpty(sessionId)) { + return false; + } + + String cacheKey = + buildCacheKey(sessionId, getPrincipal().toString(), String.valueOf(categoryId)); + return cacheStore.get(cacheKey).isPresent(); + } + + @Override + public void setAuthenticated(Integer resourceId, boolean isAuthenticated) { + String sessionId = getSessionId(); + // No session is represent a client request + if (StringUtils.isEmpty(sessionId)) { + return; + } + + String cacheKey = + buildCacheKey(sessionId, getPrincipal().toString(), String.valueOf(resourceId)); + if (isAuthenticated) { + cacheStore.putAny(cacheKey, StringUtils.EMPTY, 1, TimeUnit.DAYS); + return; + } + cacheStore.delete(cacheKey); + } + + @Override + public void clearByResourceId(Integer resourceId) { + String resourceCachePrefix = + StringUtils.joinWith(":", CACHE_PREFIX, getPrincipal(), resourceId); + cacheStore.toMap().forEach((key, value) -> { + if (StringUtils.startsWith(key, resourceCachePrefix)) { + cacheStore.delete(key); + } + }); + } +} diff --git a/src/main/java/run/halo/app/controller/content/auth/ContentAuthentication.java b/src/main/java/run/halo/app/controller/content/auth/ContentAuthentication.java new file mode 100644 index 0000000000..2e0c9e142d --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/auth/ContentAuthentication.java @@ -0,0 +1,79 @@ +package run.halo.app.controller.content.auth; + +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import org.apache.commons.lang3.StringUtils; +import org.springframework.util.Assert; +import run.halo.app.utils.ServletUtils; + +/** + * Content authentication. + * + * @author guqing + * @date 2022-02-23 + */ +public interface ContentAuthentication { + + /** + * The identity of the principal being authenticated. + * + * @return authentication principal. + */ + Object getPrincipal(); + + /** + * whether the resource been authenticated by a sessionId. + * + * @param resourceId resourceId to authentication + * @see HttpServletRequest#getRequestedSessionId() + * @return true if the resourceId has been authenticated by a sessionId + */ + boolean isAuthenticated(Integer resourceId); + + /** + * Set authentication state. + * + * @param resourceId resource identity + * @param isAuthenticated authentication state + * @see HttpServletRequest#getRequestedSessionId() + */ + void setAuthenticated(Integer resourceId, boolean isAuthenticated); + + /** + * Clear authentication state. + * + * @param resourceId resource id. + */ + void clearByResourceId(Integer resourceId); + + String CACHE_PREFIX = "CONTENT_AUTHENTICATED"; + + /** + * build authentication cache key. + * + * @param sessionId session id + * @param principal authentication principal + * @param value principal identity + * @return cache key + */ + default String buildCacheKey(String sessionId, String principal, + String value) { + Assert.notNull(sessionId, "The sessionId must not be null."); + Assert.notNull(principal, "The principal must not be null."); + Assert.notNull(value, "The value must not be null."); + return StringUtils.joinWith(":", CACHE_PREFIX, principal, value, sessionId); + } + + /** + * Gets request session id. + * + * @return request session id. + */ + default String getSessionId() { + Optional currentRequest = ServletUtils.getCurrentRequest(); + if (currentRequest.isEmpty()) { + return StringUtils.EMPTY; + } + return currentRequest.get().getRequestedSessionId(); + } +} diff --git a/src/main/java/run/halo/app/controller/content/auth/ContentAuthenticationManager.java b/src/main/java/run/halo/app/controller/content/auth/ContentAuthenticationManager.java new file mode 100644 index 0000000000..7c6aa67991 --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/auth/ContentAuthenticationManager.java @@ -0,0 +1,128 @@ +package run.halo.app.controller.content.auth; + +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import run.halo.app.event.category.CategoryUpdatedEvent; +import run.halo.app.event.post.PostUpdatedEvent; +import run.halo.app.exception.AuthenticationException; +import run.halo.app.exception.NotFoundException; +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.service.CategoryService; +import run.halo.app.service.PostCategoryService; +import run.halo.app.service.PostService; + +/** + * Content authentication manager. + * + * @author guqing + * @date 2022-02-24 + */ +@Component +public class ContentAuthenticationManager { + private final CategoryService categoryService; + private final CategoryAuthentication categoryAuthentication; + private final PostService postService; + private final PostAuthentication postAuthentication; + private final PostCategoryService postCategoryService; + + public ContentAuthenticationManager(CategoryService categoryService, + CategoryAuthentication categoryAuthentication, PostService postService, + PostAuthentication postAuthentication, + PostCategoryService postCategoryService) { + this.categoryService = categoryService; + this.categoryAuthentication = categoryAuthentication; + this.postService = postService; + this.postAuthentication = postAuthentication; + this.postCategoryService = postCategoryService; + } + + public ContentAuthentication authenticate(ContentAuthenticationRequest authRequest) throws + AuthenticationException { + if (EncryptTypeEnum.POST.getName().equals(authRequest.getPrincipal())) { + return authenticatePost(authRequest); + } + if (EncryptTypeEnum.CATEGORY.getName().equals(authRequest.getPrincipal())) { + return authenticateCategory(authRequest); + } + throw new NotFoundException( + "Could not be found suitable authentication processor for [" + + authRequest.getPrincipal() + "]"); + } + + @EventListener(CategoryUpdatedEvent.class) + public void categoryUpdatedListener(CategoryUpdatedEvent event) { + Category category = event.getCategory(); + categoryAuthentication.clearByResourceId(category.getId()); + } + + @EventListener(PostUpdatedEvent.class) + public void postUpdatedListener(PostUpdatedEvent event) { + Post post = event.getPost(); + postAuthentication.clearByResourceId(post.getId()); + } + + private PostAuthentication authenticatePost(ContentAuthenticationRequest authRequest) { + Post post = postService.getById(authRequest.getId()); + if (StringUtils.isNotBlank(post.getPassword())) { + if (StringUtils.equals(post.getPassword(), authRequest.getPassword())) { + postAuthentication.setAuthenticated(post.getId(), true); + return postAuthentication; + } else { + throw new AuthenticationException("密码不正确"); + } + } else { + List encryptedCategories = postCategoryService.listCategoriesBy(post.getId()) + .stream() + .filter(category -> categoryService.isPrivate(category.getId())) + .collect(Collectors.toList()); + // The post has no password and does not belong to any encryption categories. + // Return it directly + if (CollectionUtils.isEmpty(encryptedCategories)) { + return postAuthentication; + } + + // Try all categories until the password is correct + for (Category category : encryptedCategories) { + if (StringUtils.equals(category.getPassword(), authRequest.getPassword())) { + postAuthentication.setAuthenticated(post.getId(), true); + return postAuthentication; + } + } + throw new AuthenticationException("密码不正确"); + } + } + + private CategoryAuthentication authenticateCategory(ContentAuthenticationRequest authRequest) { + Category category = categoryService.getById(authRequest.getId()); + if (category.getPassword() == null) { + String parentPassword = categoryService.lookupFirstEncryptedBy(category.getId()) + .map(Category::getPassword) + .orElse(null); + if (parentPassword == null) { + return categoryAuthentication; + } + category.setPassword(parentPassword); + } + + if (StringUtils.equals(category.getPassword(), authRequest.getPassword())) { + categoryAuthentication.setAuthenticated(category.getId(), true); + return categoryAuthentication; + } + // Finds the first encrypted parent category to authenticate + Category parentCategory = + categoryService.lookupFirstEncryptedBy(authRequest.getId()) + .orElseThrow(() -> new AuthenticationException("密码不正确")); + if (!Objects.equals(parentCategory.getPassword(), authRequest.getPassword())) { + throw new AuthenticationException("密码不正确"); + } + categoryAuthentication.setAuthenticated(category.getId(), true); + return categoryAuthentication; + } +} diff --git a/src/main/java/run/halo/app/controller/content/auth/ContentAuthenticationRequest.java b/src/main/java/run/halo/app/controller/content/auth/ContentAuthenticationRequest.java new file mode 100644 index 0000000000..151a5dd17a --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/auth/ContentAuthenticationRequest.java @@ -0,0 +1,54 @@ +package run.halo.app.controller.content.auth; + +import lombok.Data; + +/** + * Authentication request for {@link ContentAuthenticationManager}. + * + * @author guqing + * @date 2022-02-24 + */ +@Data +public class ContentAuthenticationRequest implements ContentAuthentication { + private Integer id; + + private String password; + + private String principal; + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public boolean isAuthenticated(Integer resourceId) { + return false; + } + + @Override + public void setAuthenticated(Integer resourceId, boolean isAuthenticated) { + throw new UnsupportedOperationException(); + } + + @Override + public void clearByResourceId(Integer resourceId) { + throw new UnsupportedOperationException(); + } + + /** + * Creates a {@link ContentAuthenticationRequest}. + * + * @param id resource id + * @param password resource password + * @param principal authentication principal + * @return a {@link ContentAuthenticationRequest} instance. + */ + public static ContentAuthenticationRequest of(Integer id, String password, String principal) { + ContentAuthenticationRequest request = new ContentAuthenticationRequest(); + request.setId(id); + request.setPassword(password); + request.setPrincipal(principal); + return request; + } +} diff --git a/src/main/java/run/halo/app/controller/content/auth/PostAuthentication.java b/src/main/java/run/halo/app/controller/content/auth/PostAuthentication.java new file mode 100644 index 0000000000..a6be3f7ab1 --- /dev/null +++ b/src/main/java/run/halo/app/controller/content/auth/PostAuthentication.java @@ -0,0 +1,79 @@ +package run.halo.app.controller.content.auth; + +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; +import run.halo.app.cache.AbstractStringCacheStore; +import run.halo.app.model.entity.Post; +import run.halo.app.model.enums.EncryptTypeEnum; +import run.halo.app.service.PostService; + +/** + * Authentication for post. + * + * @author guqing + * @date 2022-02-24 + */ +@Component +public class PostAuthentication implements ContentAuthentication { + + private final PostService postService; + private final AbstractStringCacheStore cacheStore; + + public PostAuthentication(PostService postService, + AbstractStringCacheStore cacheStore) { + this.postService = postService; + this.cacheStore = cacheStore; + } + + @Override + public Object getPrincipal() { + return EncryptTypeEnum.POST.getName(); + } + + @Override + public boolean isAuthenticated(Integer postId) { + Post post = postService.getById(postId); + if (post.getPassword() == null) { + return true; + } + + String sessionId = getSessionId(); + // No session is represent a client request + if (StringUtils.isEmpty(sessionId)) { + return false; + } + + String cacheKey = + buildCacheKey(sessionId, getPrincipal().toString(), String.valueOf(postId)); + return cacheStore.get(cacheKey).isPresent(); + } + + @Override + public void setAuthenticated(Integer resourceId, boolean isAuthenticated) { + String sessionId = getSessionId(); + // No session is represent a client request + if (StringUtils.isEmpty(sessionId)) { + return; + } + + String cacheKey = + buildCacheKey(sessionId, getPrincipal().toString(), String.valueOf(resourceId)); + if (isAuthenticated) { + cacheStore.putAny(cacheKey, StringUtils.EMPTY, 1, TimeUnit.DAYS); + return; + } + cacheStore.delete(cacheKey); + } + + @Override + public void clearByResourceId(Integer resourceId) { + String resourceCachePrefix = + StringUtils.joinWith(":", CACHE_PREFIX, getPrincipal(), resourceId); + cacheStore.toMap().forEach((key, value) -> { + if (StringUtils.startsWith(key, resourceCachePrefix)) { + cacheStore.delete(key); + } + }); + } +} 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 43146b3a0f..d870e5d1df 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 @@ -13,13 +13,13 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import org.springframework.ui.Model; +import run.halo.app.controller.content.auth.CategoryAuthentication; 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; @@ -45,20 +45,20 @@ public class CategoryModel { private final OptionService optionService; - private final AuthenticationService authenticationService; + private final CategoryAuthentication categoryAuthentication; public CategoryModel(CategoryService categoryService, ThemeService themeService, PostCategoryService postCategoryService, PostService postService, OptionService optionService, - AuthenticationService authenticationService) { + CategoryAuthentication categoryAuthentication) { this.categoryService = categoryService; this.themeService = themeService; this.postCategoryService = postCategoryService; this.postService = postService; this.optionService = optionService; - this.authenticationService = authenticationService; + this.categoryAuthentication = categoryAuthentication; } /** @@ -85,9 +85,9 @@ public String list(Model model) { public String listPost(Model model, String slug, Integer page) { // Get category by slug - final Category category = categoryService.getBySlugOfNonNull(slug, true); + final Category category = categoryService.getBySlugOfNonNull(slug); - if (!authenticationService.categoryAuthentication(category.getId(), null)) { + if (!categoryAuthentication.isAuthenticated(category.getId())) { model.addAttribute("slug", category.getSlug()); model.addAttribute("type", EncryptTypeEnum.CATEGORY.getName()); if (themeService.templateExists(POST_PASSWORD_TEMPLATE + SUFFIX_FTL)) { 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 253adf4887..0858501253 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,6 +13,7 @@ import org.springframework.stereotype.Component; import org.springframework.ui.Model; import run.halo.app.cache.AbstractStringCacheStore; +import run.halo.app.controller.content.auth.PostAuthentication; import run.halo.app.exception.ForbiddenException; import run.halo.app.exception.NotFoundException; import run.halo.app.model.entity.Category; @@ -25,7 +26,6 @@ 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; @@ -63,7 +63,7 @@ public class PostModel { private final AbstractStringCacheStore cacheStore; - private final AuthenticationService authenticationService; + private final PostAuthentication postAuthentication; public PostModel(PostService postService, ThemeService themeService, @@ -74,7 +74,7 @@ public PostModel(PostService postService, TagService tagService, OptionService optionService, AbstractStringCacheStore cacheStore, - AuthenticationService authenticationService) { + PostAuthentication postAuthentication) { this.postService = postService; this.themeService = themeService; this.postCategoryService = postCategoryService; @@ -84,7 +84,7 @@ public PostModel(PostService postService, this.tagService = tagService; this.optionService = optionService; this.cacheStore = cacheStore; - this.authenticationService = authenticationService; + this.postAuthentication = postAuthentication; } public String content(Post post, String token, Model model) { @@ -105,7 +105,7 @@ public String content(Post post, String token, Model model) { // Drafts are not allowed bo be accessed by outsiders. throw new NotFoundException("查询不到该文章的信息"); } else if (PostStatus.INTIMATE.equals(post.getStatus()) - && !authenticationService.postAuthentication(post, null) + && !postAuthentication.isAuthenticated(post.getId()) ) { // Encrypted articles must has the correct password before they can be accessed. @@ -133,7 +133,7 @@ public String content(Post post, String token, Model model) { postService.getNextPost(post).ifPresent( nextPost -> model.addAttribute("nextPost", postService.convertToDetailVo(nextPost))); - List categories = postCategoryService.listCategoriesBy(post.getId(), false); + List categories = postCategoryService.listCategoriesBy(post.getId()); 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 e3e69a008f..b3e79a102a 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 void execute(Environment env, Map params, TemplateModel[] loopVars, switch (method) { case "list": env.setVariable("categories", builder.build().wrap(postCategoryService - .listCategoryWithPostCountDto(Sort.by(ASC, "priority"), false))); + .listCategoryWithPostCountDto(Sort.by(ASC, "priority")))); break; case "tree": env.setVariable("categories", builder.build() diff --git a/src/main/java/run/halo/app/event/category/CategoryUpdatedEvent.java b/src/main/java/run/halo/app/event/category/CategoryUpdatedEvent.java new file mode 100644 index 0000000000..837c6d688a --- /dev/null +++ b/src/main/java/run/halo/app/event/category/CategoryUpdatedEvent.java @@ -0,0 +1,24 @@ +package run.halo.app.event.category; + +import org.springframework.context.ApplicationEvent; +import run.halo.app.model.entity.Category; + +/** + * Category updated event. + * + * @author guqing + * @date 2022-02-24 + */ +public class CategoryUpdatedEvent extends ApplicationEvent { + + private final Category category; + + public CategoryUpdatedEvent(Object source, Category category) { + super(source); + this.category = category; + } + + public Category getCategory() { + return category; + } +} diff --git a/src/main/java/run/halo/app/event/post/PostUpdatedEvent.java b/src/main/java/run/halo/app/event/post/PostUpdatedEvent.java new file mode 100644 index 0000000000..d11fbfbea1 --- /dev/null +++ b/src/main/java/run/halo/app/event/post/PostUpdatedEvent.java @@ -0,0 +1,24 @@ +package run.halo.app.event.post; + +import org.springframework.context.ApplicationEvent; +import run.halo.app.model.entity.Post; + +/** + * Post updated event. + * + * @author guqing + * @date 2022-02-24 + */ +public class PostUpdatedEvent extends ApplicationEvent { + + private final Post post; + + public PostUpdatedEvent(Object source, Post post) { + super(source); + this.post = post; + } + + public Post getPost() { + return post; + } +} diff --git a/src/main/java/run/halo/app/listener/post/PostRefreshStatusListener.java b/src/main/java/run/halo/app/listener/post/PostRefreshStatusListener.java new file mode 100644 index 0000000000..9a2c290198 --- /dev/null +++ b/src/main/java/run/halo/app/listener/post/PostRefreshStatusListener.java @@ -0,0 +1,79 @@ +package run.halo.app.listener.post; + +import java.util.List; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import run.halo.app.event.category.CategoryUpdatedEvent; +import run.halo.app.event.post.PostUpdatedEvent; +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.service.CategoryService; +import run.halo.app.service.PostCategoryService; +import run.halo.app.service.PostService; + +/** + * Post status management. + * + * @author guqing + * @date 2022-02-28 + */ +@Component +public class PostRefreshStatusListener { + + private final PostService postService; + private final CategoryService categoryService; + private final PostCategoryService postCategoryService; + + public PostRefreshStatusListener(PostService postService, + CategoryService categoryService, + PostCategoryService postCategoryService) { + this.postService = postService; + this.categoryService = categoryService; + this.postCategoryService = postCategoryService; + } + + /** + * If the current category is encrypted, refresh all post referencing the category to + * INTIMATE status. + * + * @param event category updated event + */ + @EventListener(CategoryUpdatedEvent.class) + public void categoryUpdatedListener(CategoryUpdatedEvent event) { + Category category = event.getCategory(); + if (!categoryService.existsById(category.getId())) { + return; + } + boolean isPrivate = categoryService.isPrivate(category.getId()); + if (!isPrivate) { + return; + } + List posts = postCategoryService.listPostBy(category.getId()); + posts.forEach(post -> { + post.setStatus(PostStatus.INTIMATE); + }); + postService.updateInBatch(posts); + } + + /** + * If the post belongs to any encryption category, set the status to INTIMATE. + * + * @param event post updated event + */ + @EventListener(PostUpdatedEvent.class) + public void postUpdatedListener(PostUpdatedEvent event) { + Post post = event.getPost(); + if (!postService.existsById(post.getId())) { + return; + } + boolean isPrivate = postCategoryService.listByPostId(post.getId()) + .stream() + .anyMatch(postCategory -> categoryService.isPrivate(postCategory.getCategoryId())); + + if (isPrivate) { + post.setStatus(PostStatus.INTIMATE); + postService.update(post); + } + } +} 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 1799c8eb01..a97e3fa83c 100644 --- a/src/main/java/run/halo/app/model/params/CategoryParam.java +++ b/src/main/java/run/halo/app/model/params/CategoryParam.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils; import run.halo.app.model.dto.base.InputConverter; import run.halo.app.model.entity.Category; +import run.halo.app.model.support.NotAllowSpaceOnly; import run.halo.app.utils.SlugUtils; /** @@ -36,6 +37,7 @@ public class CategoryParam implements InputConverter { private String thumbnail; @Size(max = 255, message = "分类密码的字符长度不能超过 {max}") + @NotAllowSpaceOnly(message = "密码开头和结尾不能包含空字符串") private String password; private Integer parentId = 0; diff --git a/src/main/java/run/halo/app/model/params/PostParam.java b/src/main/java/run/halo/app/model/params/PostParam.java index da9f0e9102..64b5ed35a7 100644 --- a/src/main/java/run/halo/app/model/params/PostParam.java +++ b/src/main/java/run/halo/app/model/params/PostParam.java @@ -12,6 +12,7 @@ import run.halo.app.model.entity.Post; import run.halo.app.model.entity.PostMeta; import run.halo.app.model.enums.PostEditorType; +import run.halo.app.model.support.NotAllowSpaceOnly; import run.halo.app.utils.SlugUtils; /** @@ -47,6 +48,7 @@ public String getSlug() { @Override @Size(max = 255, message = "文章密码的字符长度不能超过 {max}") + @NotAllowSpaceOnly(message = "密码开头和结尾不能包含空字符串") public String getPassword() { return super.getPassword(); } diff --git a/src/main/java/run/halo/app/model/support/NotAllowSpaceOnly.java b/src/main/java/run/halo/app/model/support/NotAllowSpaceOnly.java new file mode 100644 index 0000000000..de6ca8c410 --- /dev/null +++ b/src/main/java/run/halo/app/model/support/NotAllowSpaceOnly.java @@ -0,0 +1,25 @@ +package run.halo.app.model.support; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import javax.validation.Constraint; +import javax.validation.Payload; + +/** + * Not allow space only validate annotation. + * + * @author guqing + * @date 2022-02-28 + */ +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = NotAllowSpaceOnlyConstraintValidator.class) +@Target(value = {ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER}) +public @interface NotAllowSpaceOnly { + String message() default "开头和结尾不允许包含空格"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/run/halo/app/model/support/NotAllowSpaceOnlyConstraintValidator.java b/src/main/java/run/halo/app/model/support/NotAllowSpaceOnlyConstraintValidator.java new file mode 100644 index 0000000000..2b58f912a8 --- /dev/null +++ b/src/main/java/run/halo/app/model/support/NotAllowSpaceOnlyConstraintValidator.java @@ -0,0 +1,24 @@ +package run.halo.app.model.support; + +import java.util.Objects; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import org.apache.commons.lang3.StringUtils; + +/** + * Not allow space only validator but allow "". + * + * @author guqing + * @date 2022-02-28 + */ +public class NotAllowSpaceOnlyConstraintValidator implements + ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (Objects.equals(value, "")) { + return true; + } + return StringUtils.equals(StringUtils.trim(value), value); + } +} diff --git a/src/main/java/run/halo/app/service/AuthenticationService.java b/src/main/java/run/halo/app/service/AuthenticationService.java deleted file mode 100644 index f265640fe9..0000000000 --- a/src/main/java/run/halo/app/service/AuthenticationService.java +++ /dev/null @@ -1,31 +0,0 @@ -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 deleted file mode 100644 index 79d17520bd..0000000000 --- a/src/main/java/run/halo/app/service/AuthorizationService.java +++ /dev/null @@ -1,65 +0,0 @@ -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 a0e37fa96b..95876421cd 100755 --- a/src/main/java/run/halo/app/service/CategoryService.java +++ b/src/main/java/run/halo/app/service/CategoryService.java @@ -1,8 +1,7 @@ package run.halo.app.service; -import io.swagger.models.auth.In; -import java.util.Collection; import java.util.List; +import java.util.Optional; import org.springframework.data.domain.Sort; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -58,16 +57,6 @@ 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. * @@ -85,14 +74,6 @@ 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. * @@ -109,34 +90,6 @@ public interface CategoryService extends CrudService { */ List listAllByParentId(@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 category not encrypt. - * - * @param queryEncryptCategory whether to query encryption category - * @return list of category. - */ - List listAll(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. * @@ -156,22 +109,22 @@ public interface CategoryService extends CrudService { List convertTo(@Nullable List categories); /** - * Filter encrypt category + * Determine whether the category is encrypted. * - * @param categories this categories is not a category list tree - * @return category list + * @param categoryId category id + * @return whether to encrypt */ - @NonNull - List filterEncryptCategory(@Nullable List categories); + boolean isPrivate(Integer categoryId); /** - * Determine whether the category is encrypted. + * This method will first query all categories and create a tree, then start from the node + * whose ID is categoryId and recursively look up the first encryption category. * - * @param categoryId category id - * @return whether to encrypt + * @param categoryId categoryId to look up + * @return encrypted immediate parent category If it is found,otherwise an empty + * {@code Optional}. */ - @NonNull - Boolean categoryHasEncrypt(Integer categoryId); + Optional lookupFirstEncryptedBy(Integer categoryId); /** * Use categories to build a category tree. diff --git a/src/main/java/run/halo/app/service/PostCategoryService.java b/src/main/java/run/halo/app/service/PostCategoryService.java index 217cd16ea9..ad182419df 100644 --- a/src/main/java/run/halo/app/service/PostCategoryService.java +++ b/src/main/java/run/halo/app/service/PostCategoryService.java @@ -36,26 +36,14 @@ 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, @NonNull boolean queryEncryptCategory); + Map> listCategoryListMap(@Nullable Collection postIds); /** * Lists post by category id. @@ -202,12 +190,10 @@ List mergeOrCreateByIfAbsent(@NonNull Integer postId, * 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, @NonNull boolean queryEncryptCategory); + List listCategoryWithPostCountDto(@NonNull Sort sort); /** * Lists by category id. diff --git a/src/main/java/run/halo/app/service/PostService.java b/src/main/java/run/halo/app/service/PostService.java index 7e8283efea..bb0a9d47c2 100755 --- a/src/main/java/run/halo/app/service/PostService.java +++ b/src/main/java/run/halo/app/service/PostService.java @@ -286,15 +286,6 @@ Post getBy(@NonNull Integer year, @NonNull Integer month, @NonNull Integer day, @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/base/BasePostService.java b/src/main/java/run/halo/app/service/base/BasePostService.java index b2cd44c5fa..c56aa2c890 100644 --- a/src/main/java/run/halo/app/service/base/BasePostService.java +++ b/src/main/java/run/halo/app/service/base/BasePostService.java @@ -224,15 +224,6 @@ public interface BasePostService extends CrudService accessPermissionStore = authorizationService.getAccessPermissionStore(); - - if (StringUtils.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 (StringUtils.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 deleted file mode 100644 index 188a23c0de..0000000000 --- a/src/main/java/run/halo/app/service/impl/AuthorizationServiceImpl.java +++ /dev/null @@ -1,108 +0,0 @@ -package run.halo.app.service.impl; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.Map.Entry; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import javax.servlet.http.HttpServletRequest; -import lombok.extern.slf4j.Slf4j; -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; -import run.halo.app.utils.JsonUtils; - -/** - * @author ZhiXiang Yuan - * @author guqing - * @date 2021/01/21 11:28 - */ -@Slf4j -@Service -public class AuthorizationServiceImpl implements AuthorizationService { - private static final String ACCESS_PERMISSION_PREFIX = "ACCESS_PERMISSION: "; - 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).orElseGet(HashSet::new); - } - - @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); - - for (Entry entry : cacheStore.toMap().entrySet()) { - String key = entry.getKey(); - if (!key.startsWith(ACCESS_PERMISSION_PREFIX)) { - continue; - } - Set valueSet = jsonToValueSet(entry.getValue()); - if (valueSet.contains(value)) { - valueSet.remove(value); - cacheStore.putAny(key, valueSet, 1, TimeUnit.DAYS); - } - } - } - - private Set jsonToValueSet(String json) { - try { - return JsonUtils.DEFAULT_JSON_MAPPER.readValue(json, - new TypeReference>() { - }); - } catch (JsonProcessingException e) { - log.warn("Failed to convert json to authorization cache value set: [{}]", json, e); - } - return Collections.emptySet(); - } - - 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_PREFIX + request.getSession().getId(); - } - -} diff --git a/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java b/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java index 7024eece9c..e2e395592b 100644 --- a/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BackupServiceImpl.java @@ -348,7 +348,7 @@ public BackupDTO exportData() { data.put("version", HaloConst.HALO_VERSION); data.put("export_date", DateUtils.now()); data.put("attachments", attachmentService.listAll()); - data.put("categories", categoryService.listAll(true)); + data.put("categories", categoryService.listAll()); data.put("comment_black_list", commentBlackListService.listAll()); data.put("journals", journalService.listAll()); data.put("journal_comments", journalCommentService.listAll()); 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 5a57f2d9c0..571ec95a6f 100644 --- a/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/BasePostServiceImpl.java @@ -329,23 +329,6 @@ public POST createOrUpdateBy(POST post) { return savedPost; } - @Override - public POST filterIfEncrypt(POST post) { - Assert.notNull(post, "Post must not be null"); - - if (StringUtils.isNotBlank(post.getPassword())) { - String tip = "The post is encrypted by author"; - post.setSummary(tip); - - Content postContent = new Content(); - postContent.setContent(tip); - postContent.setOriginalContent(tip); - post.setContent(PatchedContent.of(postContent)); - } - - return post; - } - @Override public BasePostMinimalDTO convertToMinimal(POST post) { Assert.notNull(post, "Post must not be null"); 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 5302f1f3cd..437b21b44c 100644 --- a/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/CategoryServiceImpl.java @@ -2,25 +2,20 @@ import static run.halo.app.model.support.HaloConst.URL_SEPARATOR; -import com.google.common.base.Objects; import java.util.ArrayDeque; -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.Objects; import java.util.Optional; import java.util.Queue; import java.util.Set; -import java.util.function.Function; import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -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.context.ApplicationContext; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.lang.NonNull; @@ -28,22 +23,16 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import run.halo.app.event.category.CategoryUpdatedEvent; 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.HaloUtils; @@ -68,29 +57,17 @@ public class CategoryServiceImpl extends AbstractCrudService private final OptionService optionService; - private final AuthorizationService authorizationService; - - private PostService postService; - - private final AuthenticationService authenticationService; + private final ApplicationContext applicationContext; public CategoryServiceImpl(CategoryRepository categoryRepository, PostCategoryService postCategoryService, OptionService optionService, - AuthenticationService authenticationService, - AuthorizationService authorizationService) { + ApplicationContext applicationContext) { 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; + this.applicationContext = applicationContext; } @Override @@ -126,6 +103,13 @@ public Category create(Category category) { return super.create(category); } + @Override + public Category update(Category category) { + Category updated = super.update(category); + applicationContext.publishEvent(new CategoryUpdatedEvent(this, category)); + return updated; + } + @Override public List listAsTree(Sort sort) { Assert.notNull(sort, "Sort info must not be null"); @@ -167,6 +151,11 @@ public String buildCategoryFullPath(@NonNull String slug) { return fullPath.toString(); } + @Override + public Category getBySlug(String slug) { + return categoryRepository.getBySlug(slug).orElse(null); + } + @NonNull private CategoryVO convertToCategoryVo(Category category) { Assert.notNull(category, "The category must not be null."); @@ -175,60 +164,16 @@ private CategoryVO convertToCategoryVo(Category category) { return categoryVo; } - @Override - public Category getBySlug(String slug) { - 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) { - - Category category = categoryRepository + return 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) { - Optional byName = categoryRepository.getByName(name); - if (byName.isEmpty()) { - return null; - } - - Category category = byName.get(); - - if (authenticationService.categoryAuthentication(category.getId(), null)) { - return category; - } - - return null; + return categoryRepository.getByName(name).orElse(null); } @Override @@ -243,42 +188,11 @@ public void removeCategoryAndPostCategoryBy(Integer categoryId) { } // Remove category - removeById(categoryId); + Category category = removeById(categoryId); // Remove post categories - List affectedPostIdList = postCategoryService.removeByCategoryId(categoryId) - .stream().map(PostCategory::getPostId).collect(Collectors.toList()); - - refreshPostStatus(affectedPostIdList); - } - - @Override - public void refreshPostStatus(List affectedPostIdList) { - if (CollectionUtils.isEmpty(affectedPostIdList)) { - return; - } - - for (Integer postId : affectedPostIdList) { - Post post = postService.getById(postId); - - post.setStatus(null); - - if (StringUtils.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); - } + postCategoryService.removeByCategoryId(categoryId); + applicationContext.publishEvent(new CategoryUpdatedEvent(this, category)); } @Override @@ -336,7 +250,7 @@ private Optional findCategoryTreeNodeById(List categoryV Queue queue = new ArrayDeque<>(categoryVos); while (!queue.isEmpty()) { CategoryVO category = queue.poll(); - if (Objects.equal(category.getId(), categoryId)) { + if (Objects.equals(category.getId(), categoryId)) { return Optional.of(category); } if (HaloUtils.isNotEmpty(category.getChildren())) { @@ -369,266 +283,8 @@ public List convertTo(List categories) { } @Override - public List filterEncryptCategory(List categories) { - if (CollectionUtils.isEmpty(categories)) { - return Collections.emptyList(); - } - - // list to tree, no password desensitise is required here - List categoryTree = listToTree(categories); - - // filter encrypt category - doFilterEncryptCategory(categoryTree); - - List collectorList = new ArrayList<>(); - - collectAllChild(collectorList, categoryTree, true); - - for (Category category : collectorList) { - category.setPassword(null); - } - - return collectorList; - } - - /** - * do filter encrypt category - * - * @param categoryList category list - */ - private void doFilterEncryptCategory(List categoryList) { - if (CollectionUtils.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 (CollectionUtils.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 (HaloUtils.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 (CollectionUtils.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(boolean queryEncryptCategory) { - if (queryEncryptCategory) { - return super.listAll(); - } else { - return this.listAll(); - } - } - - @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 (StringUtils.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) { - // list to tree with password - List categoryTree = listToTree(super.listAll()); - - List collectorList = new ArrayList<>(); - - collectAllChildByCategoryId(collectorList, - categoryTree, 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; - } - // with password - List categoryTree = listToTree(allCategoryList); - - List collectorList = new ArrayList<>(); - - // Only collect unencrypted sub-categories under the category. - collectAllChildByCategoryId(collectorList, - categoryTree, 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 -> StringUtils.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); + public boolean isPrivate(Integer categoryId) { + return lookupFirstEncryptedBy(categoryId).isPresent(); } @Override @@ -658,30 +314,38 @@ public List listToTree(List categories) { .collect(Collectors.toList()); } + @Override + public Optional lookupFirstEncryptedBy(Integer categoryId) { + List categories = listAll(); + Map categoryMap = + ServiceUtils.convertToMap(categories, Category::getId); + return Optional.ofNullable(findFirstEncryptedCategoryBy(categoryMap, categoryId)); + } + /** * Find whether the parent category is encrypted. + * If it is found, the result will be returned immediately. + * Otherwise, it will be found recursively according to parentId. * * @param idToCategoryMap find category by id * @param categoryId category id * @return whether to encrypt */ - private boolean doCategoryHasEncrypt( - Map idToCategoryMap, Integer categoryId) { + private Category findFirstEncryptedCategoryBy(Map idToCategoryMap, + Integer categoryId) { + Category category = idToCategoryMap.get(categoryId); - if (categoryId == 0) { - return false; + if (categoryId == 0 || category == null) { + return null; } - Category category = idToCategoryMap.get(categoryId); - if (StringUtils.isNotBlank(category.getPassword())) { - return true; + return category; } - return doCategoryHasEncrypt(idToCategoryMap, category.getParentId()); + return findFirstEncryptedCategoryBy(idToCategoryMap, category.getParentId()); } - @Override @Transactional(rollbackFor = Exception.class) public List updateInBatch(Collection categories) { @@ -696,8 +360,12 @@ public List updateInBatch(Collection categories) { .map(categoryToUpdate -> { Category categoryParam = idCategoryParamMap.get(categoryToUpdate.getId()); BeanUtils.updateProperties(categoryParam, categoryToUpdate); - return update(categoryToUpdate); + Category categoryUpdated = update(categoryToUpdate); + applicationContext.publishEvent(new CategoryUpdatedEvent(this, categoryUpdated)); + return categoryUpdated; }) .collect(Collectors.toList()); } + + } 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 220d0b8a67..bb2cda77ca 100644 --- a/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/PostCategoryServiceImpl.java @@ -76,23 +76,18 @@ public void setCategoryService(CategoryService categoryService) { @Override public List 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 categoryService.listAllByIds(categoryIds, queryEncryptCategory); + return categoryService.listAllByIds(categoryIds); } @Override public Map> listCategoryListMap( - Collection postIds, boolean queryEncryptCategory) { + Collection postIds) { if (CollectionUtils.isEmpty(postIds)) { return Collections.emptyMap(); } @@ -105,7 +100,7 @@ public Map> listCategoryListMap( ServiceUtils.fetchProperty(postCategories, PostCategory::getCategoryId); // Find all categories - List categories = categoryService.listAllByIds(categoryIds, queryEncryptCategory); + List categories = categoryService.listAllByIds(categoryIds); // Convert to category map Map categoryMap = ServiceUtils.convertToMap(categories, Category::getId); @@ -309,10 +304,9 @@ public List removeByCategoryId(Integer categoryId) { } @Override - public List listCategoryWithPostCountDto(@NonNull Sort sort, - boolean queryEncryptCategory) { + public List listCategoryWithPostCountDto(@NonNull Sort sort) { Assert.notNull(sort, "Sort info must not be null"); - List categories = categoryService.listAll(sort, queryEncryptCategory); + List categories = categoryService.listAll(sort); List categoryTreeVo = categoryService.listToTree(categories); populatePostIds(categoryTreeVo); 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 4dcc021a36..a3ce877988 100644 --- a/src/main/java/run/halo/app/service/impl/PostServiceImpl.java +++ b/src/main/java/run/halo/app/service/impl/PostServiceImpl.java @@ -22,6 +22,7 @@ import javax.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -34,6 +35,7 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import run.halo.app.event.logger.LogEvent; +import run.halo.app.event.post.PostUpdatedEvent; import run.halo.app.event.post.PostVisitEvent; import run.halo.app.exception.NotFoundException; import run.halo.app.model.dto.post.BasePostMinimalDTO; @@ -61,7 +63,6 @@ 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.ContentPatchLogService; import run.halo.app.service.ContentService; @@ -113,10 +114,10 @@ public class PostServiceImpl extends BasePostServiceImpl implements PostSe private final OptionService optionService; - private final AuthorizationService authorizationService; - private final ContentPatchLogService postContentPatchLogService; + private final ApplicationContext applicationContext; + public PostServiceImpl(BasePostRepository basePostRepository, OptionService optionService, PostRepository postRepository, @@ -127,9 +128,9 @@ public PostServiceImpl(BasePostRepository basePostRepository, PostCommentService postCommentService, ApplicationEventPublisher eventPublisher, PostMetaService postMetaService, - AuthorizationService authorizationService, ContentService contentService, - ContentPatchLogService contentPatchLogService) { + ContentPatchLogService contentPatchLogService, + ApplicationContext applicationContext) { super(basePostRepository, optionService, contentService, contentPatchLogService); this.postRepository = postRepository; this.tagService = tagService; @@ -140,9 +141,9 @@ public PostServiceImpl(BasePostRepository basePostRepository, this.eventPublisher = eventPublisher; this.postMetaService = postMetaService; this.optionService = optionService; - this.authorizationService = authorizationService; this.postContentService = contentService; this.postContentPatchLogService = contentPatchLogService; + this.applicationContext = applicationContext; } @Override @@ -565,7 +566,7 @@ public PostDetailVO convertToDetailVo(Post post, boolean queryEncryptCategory) { List tags = postTagService.listTagsBy(post.getId()); // List categories List categories = postCategoryService - .listCategoriesBy(post.getId(), queryEncryptCategory); + .listCategoriesBy(post.getId()); // List metas List metas = postMetaService.listBy(post.getId()); // Convert to detail vo @@ -633,7 +634,7 @@ public Page convertToListVo(Page postPage, boolean queryEncryp // Get category list map Map> categoryListMap = postCategoryService - .listCategoryListMap(postIds, queryEncryptCategory); + .listCategoryListMap(postIds); // Get comment count Map commentCountMap = postCommentService.countByStatusAndPostIds( @@ -685,11 +686,6 @@ public Page convertToListVo(Page postPage, boolean queryEncryp @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); @@ -699,7 +695,7 @@ public List convertToListVo(List posts, boolean queryEncryptCa // Get category list map Map> categoryListMap = postCategoryService - .listCategoryListMap(postIds, queryEncryptCategory); + .listCategoryListMap(postIds); // Get comment count Map commentCountMap = @@ -885,21 +881,9 @@ private PostDetailVO createOrUpdate(@NonNull Post post, Set tagIds, Set categoryIds, Set metas) { Assert.notNull(post, "Post param must not be null"); - // Create or update post - Boolean needEncrypt = Optional.ofNullable(categoryIds) - .filter(HaloUtils::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 password is not empty if (post.getStatus() != PostStatus.DRAFT - && (StringUtils.isNotEmpty(post.getPassword()) || needEncrypt) + && (StringUtils.isNotEmpty(post.getPassword())) ) { post.setStatus(PostStatus.INTIMATE); } @@ -914,7 +898,7 @@ private PostDetailVO createOrUpdate(@NonNull Post post, Set tagIds, List tags = tagService.listAllByIds(tagIds); // List all categories - List categories = categoryService.listAllByIds(categoryIds, true); + List categories = categoryService.listAllByIds(categoryIds); // Create post tags List postTags = postTagService.mergeOrCreateByIfAbsent(post.getId(), @@ -934,8 +918,8 @@ private PostDetailVO createOrUpdate(@NonNull Post post, Set tagIds, .createOrUpdateByPostId(post.getId(), metas); log.debug("Created post metas: [{}]", postMetaList); - // Remove authorization every time an post is created or updated. - authorizationService.deletePostAuthorization(post.getId()); + // Publish post updated event. + applicationContext.publishEvent(new PostUpdatedEvent(this, post)); // get draft content by head patch log id Content postContent = postContentService.getById(post.getId()); @@ -945,27 +929,6 @@ private PostDetailVO createOrUpdate(@NonNull Post post, Set tagIds, 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 7f70f7f3be..2a48009ec0 100644 --- a/src/main/resources/templates/common/template/post_password.ftl +++ b/src/main/resources/templates/common/template/post_password.ftl @@ -160,6 +160,7 @@ +
${errorMsg!}
diff --git a/src/test/java/run/halo/app/controller/content/CategoryAuthenticationTest.java b/src/test/java/run/halo/app/controller/content/CategoryAuthenticationTest.java new file mode 100644 index 0000000000..a88773691d --- /dev/null +++ b/src/test/java/run/halo/app/controller/content/CategoryAuthenticationTest.java @@ -0,0 +1,81 @@ +package run.halo.app.controller.content; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import run.halo.app.cache.InMemoryCacheStore; +import run.halo.app.controller.content.auth.CategoryAuthentication; +import run.halo.app.model.entity.Category; +import run.halo.app.model.enums.EncryptTypeEnum; +import run.halo.app.service.CategoryService; + +/** + * @author guqing + * @date 2022-02-25 + */ +@ExtendWith(SpringExtension.class) +public class CategoryAuthenticationTest { + + private CategoryAuthentication categoryAuthentication; + + @MockBean + private CategoryService categoryService; + + private final InMemoryCacheStore inMemoryCacheStore = new InMemoryCacheStore(); + + @BeforeEach + public void setUp() { + categoryAuthentication = new CategoryAuthentication(categoryService, inMemoryCacheStore); + + Category category = new Category(); + category.setId(1); + category.setSlug("slug-1"); + category.setName("name-1"); + category.setDescription("description-1"); + category.setPassword("123"); + when(categoryService.getById(1)).thenReturn(category); + } + + @Test + public void isAuthenticated() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestedSessionId("mock_session_id"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + boolean authenticated = categoryAuthentication.isAuthenticated(1); + assertThat(authenticated).isFalse(); + + categoryAuthentication.setAuthenticated(1, true); + assertThat(categoryAuthentication.isAuthenticated(1)).isTrue(); + + categoryAuthentication.clearByResourceId(1); + assertThat(categoryAuthentication.isAuthenticated(1)).isFalse(); + } + + @Test + public void buildCacheKeyTest() { + String cacheKey = categoryAuthentication.buildCacheKey("mock_session_id", + EncryptTypeEnum.CATEGORY.getName(), "1"); + assertThat(cacheKey).isEqualTo("CONTENT_AUTHENTICATED:category:1:mock_session_id"); + } + + @Test + public void getSessionIdTest() { + String sessionId = categoryAuthentication.getSessionId(); + assertThat(sessionId).isEqualTo(""); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestedSessionId("mock_session_id"); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request)); + + assertThat(categoryAuthentication.getSessionId()).isEqualTo("mock_session_id"); + } +} diff --git a/src/test/java/run/halo/app/controller/content/ContentAuthenticationManagerTest.java b/src/test/java/run/halo/app/controller/content/ContentAuthenticationManagerTest.java new file mode 100644 index 0000000000..e5634d6444 --- /dev/null +++ b/src/test/java/run/halo/app/controller/content/ContentAuthenticationManagerTest.java @@ -0,0 +1,110 @@ +package run.halo.app.controller.content; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.when; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import run.halo.app.controller.content.auth.CategoryAuthentication; +import run.halo.app.controller.content.auth.ContentAuthentication; +import run.halo.app.controller.content.auth.ContentAuthenticationManager; +import run.halo.app.controller.content.auth.ContentAuthenticationRequest; +import run.halo.app.controller.content.auth.PostAuthentication; +import run.halo.app.exception.AuthenticationException; +import run.halo.app.model.entity.Category; +import run.halo.app.model.enums.EncryptTypeEnum; +import run.halo.app.service.CategoryService; +import run.halo.app.service.PostCategoryService; +import run.halo.app.service.PostService; + +/** + * Test for {@link run.halo.app.controller.content.auth.ContentAuthenticationManager}. + * + * @author guqing + * @date 2022-02-26 + */ +@ExtendWith(SpringExtension.class) +public class ContentAuthenticationManagerTest { + @MockBean + private CategoryService categoryService; + + @MockBean + private CategoryAuthentication categoryAuthentication; + + @MockBean + private PostService postService; + + @MockBean + private PostAuthentication postAuthentication; + + @MockBean + private PostCategoryService postCategoryService; + + private ContentAuthenticationManager contentAuthenticationManager; + + @BeforeEach + public void setUp() { + contentAuthenticationManager = + new ContentAuthenticationManager(categoryService, categoryAuthentication, postService, + postAuthentication, postCategoryService); + } + + @Test + public void authenticateCategoryTest() { + /* + * category-1(加密) + * | |-category-2(未设密码) + */ + Category category1 = new Category(); + category1.setId(1); + category1.setPassword("123"); + category1.setName("category-1"); + category1.setSlug("category-1"); + category1.setParentId(0); + + Category category2 = new Category(); + category2.setId(2); + category2.setPassword(null); + category2.setName("category-2"); + category2.setSlug("category-2"); + category2.setParentId(1); + + // piling object + when(categoryService.lookupFirstEncryptedBy(2)) + .thenReturn(Optional.of(category1)); + when(categoryService.getById(1)).thenReturn(category1); + when(categoryService.getById(2)).thenReturn(category2); + + // build parameter + ContentAuthenticationRequest authRequest = + ContentAuthenticationRequest.of(2, "", EncryptTypeEnum.CATEGORY.getName()); + + // test empty password + assertThatThrownBy(() -> contentAuthenticationManager.authenticate(authRequest)) + .isInstanceOf(AuthenticationException.class) + .hasMessage("密码不正确"); + + // test null password + authRequest.setPassword(null); + assertThatThrownBy(() -> contentAuthenticationManager.authenticate(authRequest)) + .isInstanceOf(AuthenticationException.class) + .hasMessage("密码不正确"); + + // test incorrect password + authRequest.setPassword("ABCD"); + assertThatThrownBy(() -> contentAuthenticationManager.authenticate(authRequest)) + .isInstanceOf(AuthenticationException.class) + .hasMessage("密码不正确"); + + // test correct password + authRequest.setPassword("123"); + ContentAuthentication authentication = + contentAuthenticationManager.authenticate(authRequest); + assertThat(authentication).isNotNull(); + } +} diff --git a/src/test/java/run/halo/app/model/params/PostParamTest.java b/src/test/java/run/halo/app/model/params/PostParamTest.java index c89eb8bc67..8098ef85fb 100644 --- a/src/test/java/run/halo/app/model/params/PostParamTest.java +++ b/src/test/java/run/halo/app/model/params/PostParamTest.java @@ -43,6 +43,34 @@ public void validationTest() { assertThat(validate.iterator().next().getMessage()).isEqualTo("排序字段值不能小于 0"); } + @Test + public void validatePassword() { + PostParam postParam = new PostParam(); + postParam.setTitle("Title"); + postParam.setSlug("Slug"); + postParam.setPassword(" 123"); + postParam.setTopPriority(0); + + Set> validate = validator.validate(postParam); + assertThat(validate).isNotNull(); + assertThat(validate).hasSize(1); + assertThat(validate.iterator().next().getMessage()).isEqualTo("密码开头和结尾不能包含空字符串"); + + postParam.setPassword("123 "); + validate = validator.validate(postParam); + assertThat(validate).isNotNull(); + assertThat(validate).hasSize(1); + assertThat(validate.iterator().next().getMessage()).isEqualTo("密码开头和结尾不能包含空字符串"); + + postParam.setPassword(""); + validate = validator.validate(postParam); + assertThat(validate).isEmpty(); + + postParam.setPassword("123 hello"); + validate = validator.validate(postParam); + assertThat(validate).isEmpty(); + } + @Test public void convertToTest() { PostParam postParam = new PostParam(); diff --git a/src/test/java/run/halo/app/service/impl/AuthorizationServiceImplTest.java b/src/test/java/run/halo/app/service/impl/AuthorizationServiceImplTest.java deleted file mode 100644 index bb5b8a6f61..0000000000 --- a/src/test/java/run/halo/app/service/impl/AuthorizationServiceImplTest.java +++ /dev/null @@ -1,129 +0,0 @@ -package run.halo.app.service.impl; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import com.fasterxml.jackson.core.JsonProcessingException; -import java.util.Set; -import org.apache.commons.lang3.StringUtils; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.mock.web.MockHttpServletRequest; -import org.springframework.mock.web.MockHttpSession; -import org.springframework.mock.web.MockServletContext; -import org.springframework.web.context.request.RequestContextHolder; -import org.springframework.web.context.request.ServletRequestAttributes; -import run.halo.app.cache.InMemoryCacheStore; -import run.halo.app.utils.JsonUtils; - -/** - * @author guqing - * @date 2021-11-19 - */ -public class AuthorizationServiceImplTest { - - private AuthorizationServiceImpl authorizationService; - private InMemoryCacheStore inMemoryCacheStore; - - @BeforeEach - public void setUp() { - inMemoryCacheStore = new InMemoryCacheStore(); - authorizationService = new AuthorizationServiceImpl(inMemoryCacheStore); - } - - @Test - public void deletePostAuthorizationTest() { - inMemoryCacheStore.clear(); - RequestContextHolder.setRequestAttributes(mockRequestAttributes("1")); - - authorizationService.postAuthorization(1); - authorizationService.postAuthorization(2); - - Set permissions = authorizationService.getAccessPermissionStore(); - assertEquals("[POST:1, POST:2]", permissions.toString()); - - authorizationService.deletePostAuthorization(1); - Set permissionsAfterDelete = authorizationService.getAccessPermissionStore(); - assertEquals("[POST:2]", permissionsAfterDelete.toString()); - - RequestContextHolder.resetRequestAttributes(); - inMemoryCacheStore.clear(); - } - - @Test - public void complexityOfDeletePostAuthorizationTest() { - inMemoryCacheStore.clear(); - // simulate session of user 1 - RequestContextHolder.setRequestAttributes(mockRequestAttributes("1")); - // user 1 accessed two encrypted posts - authorizationService.postAuthorization(1); - authorizationService.postAuthorization(2); - - // simulate session of user 2 - RequestContextHolder.setRequestAttributes(mockRequestAttributes("2")); - - // user 2 accessed two encrypted posts - authorizationService.postAuthorization(2); - authorizationService.postAuthorization(3); - - assertEquals(objectToJson(inMemoryCacheStore.toMap()), - "{\"ACCESS_PERMISSION: 2\":\"[\\\"POST:3\\\",\\\"POST:2\\\"]\"," - + "\"ACCESS_PERMISSION: 1\":\"[\\\"POST:1\\\",\\\"POST:2\\\"]\"}"); - - // simulate the admin user to change the post password - authorizationService.deletePostAuthorization(2); - - assertEquals(objectToJson(inMemoryCacheStore.toMap()), - "{\"ACCESS_PERMISSION: 2\":\"[\\\"POST:3\\\"]\"," - + "\"ACCESS_PERMISSION: 1\":\"[\\\"POST:1\\\"]\"}"); - - RequestContextHolder.resetRequestAttributes(); - inMemoryCacheStore.clear(); - } - - @Test - public void deleteCategoryAuthorizationTest() { - inMemoryCacheStore.clear(); - // simulate session of user 1 - RequestContextHolder.setRequestAttributes(mockRequestAttributes("1")); - // user 1 accessed two encrypted posts - authorizationService.categoryAuthorization(1); - authorizationService.categoryAuthorization(2); - - // simulate session of user 2 - RequestContextHolder.setRequestAttributes(mockRequestAttributes("2")); - // user 2 accessed two encrypted categories - authorizationService.categoryAuthorization(1); - authorizationService.categoryAuthorization(3); - - assertEquals(objectToJson(inMemoryCacheStore.toMap()), - "{\"ACCESS_PERMISSION: 2\":\"[\\\"CATEGORY:1\\\",\\\"CATEGORY:3\\\"]\"," - + "\"ACCESS_PERMISSION: 1\":\"[\\\"CATEGORY:1\\\",\\\"CATEGORY:2\\\"]\"}"); - - // simulate the admin user to change the category password of No.1 - authorizationService.deleteCategoryAuthorization(1); - - assertEquals(objectToJson(inMemoryCacheStore.toMap()), - "{\"ACCESS_PERMISSION: 2\":\"[\\\"CATEGORY:3\\\"]\"," - + "\"ACCESS_PERMISSION: 1\":\"[\\\"CATEGORY:2\\\"]\"}"); - - RequestContextHolder.resetRequestAttributes(); - inMemoryCacheStore.clear(); - } - - private ServletRequestAttributes mockRequestAttributes(String sessionId) { - MockHttpServletRequest request = new MockHttpServletRequest(); - MockServletContext context = new MockServletContext(); - MockHttpSession session = new MockHttpSession(context, sessionId); - request.setSession(session); - return new ServletRequestAttributes(request); - } - - private String objectToJson(Object o) { - try { - return JsonUtils.objectToJson(o); - } catch (JsonProcessingException e) { - // ignore this - } - return StringUtils.EMPTY; - } -}