refactor: post and cateogry authentication (#1678)

pull/1692/head
guqing 2022-03-01 13:32:52 +08:00 committed by GitHub
parent 5064837cf8
commit 4f1a3c5f0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1035 additions and 1069 deletions

View File

@ -57,10 +57,10 @@ public class CategoryController {
@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")

View File

@ -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.bind.annotation.RequestParam;
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.PhotoModel;
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 class ContentContentController {
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 class ContentContentController {
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 class ContentContentController {
@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 @@ public class ContentContentController {
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();
}
}

View File

@ -5,23 +5,28 @@ import static org.springframework.data.domain.Sort.Direction.DESC;
import com.google.common.collect.Sets;
import io.swagger.annotations.ApiOperation;
import java.util.List;
import 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 class CategoryController {
@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 class CategoryController {
@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<PostStatus> statusesToQuery = Sets.immutableEnumSet(PostStatus.PUBLISHED);
if (allowIntimatePosts(category.getId(), password)) {
statusesToQuery = Sets.immutableEnumSet(PostStatus.PUBLISHED, PostStatus.INTIMATE);
}
Page<Post> 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("您没有该分类的访问权限");
}
}

View File

@ -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);
}
});
}
}

View File

@ -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<HttpServletRequest> currentRequest = ServletUtils.getCurrentRequest();
if (currentRequest.isEmpty()) {
return StringUtils.EMPTY;
}
return currentRequest.get().getRequestedSessionId();
}
}

View File

@ -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<Category> 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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
});
}
}

View File

@ -13,13 +13,13 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import run.halo.app.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 class CategoryModel {
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)) {

View File

@ -13,6 +13,7 @@ import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import run.halo.app.cache.AbstractStringCacheStore;
import run.halo.app.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.EncryptTypeEnum;
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 class PostModel {
TagService tagService,
OptionService optionService,
AbstractStringCacheStore cacheStore,
AuthenticationService authenticationService) {
PostAuthentication postAuthentication) {
this.postService = postService;
this.themeService = themeService;
this.postCategoryService = postCategoryService;
@ -84,7 +84,7 @@ public class PostModel {
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 class PostModel {
// 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 class PostModel {
postService.getNextPost(post).ifPresent(
nextPost -> model.addAttribute("nextPost", postService.convertToDetailVo(nextPost)));
List<Category> categories = postCategoryService.listCategoriesBy(post.getId(), false);
List<Category> categories = postCategoryService.listCategoriesBy(post.getId());
List<Tag> tags = postTagService.listTagsBy(post.getId());
List<PostMeta> metas = postMetaService.listBy(post.getId());

View File

@ -51,7 +51,7 @@ public class CategoryTagDirective implements TemplateDirectiveModel {
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()

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<Post> 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);
}
}
}

View File

@ -7,6 +7,7 @@ import lombok.Data;
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<Category> {
private String thumbnail;
@Size(max = 255, message = "分类密码的字符长度不能超过 {max}")
@NotAllowSpaceOnly(message = "密码开头和结尾不能包含空字符串")
private String password;
private Integer parentId = 0;

View File

@ -12,6 +12,7 @@ import run.halo.app.model.dto.base.InputConverter;
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 class PostParam extends BasePostParam implements InputConverter<Post> {
@Override
@Size(max = 255, message = "文章密码的字符长度不能超过 {max}")
@NotAllowSpaceOnly(message = "密码开头和结尾不能包含空字符串")
public String getPassword() {
return super.getPassword();
}

View File

@ -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<? extends Payload>[] payload() default {};
}

View File

@ -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<NotAllowSpaceOnly, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (Objects.equals(value, "")) {
return true;
}
return StringUtils.equals(StringUtils.trim(value), value);
}
}

View File

@ -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);
}

View File

@ -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<String> getAccessPermissionStore();
/**
* Delete article authorization
*
* @param postId post id
*/
void deletePostAuthorization(Integer postId);
/**
* Delete category Authorization
*
* @param categoryId category id
*/
void deleteCategoryAuthorization(Integer categoryId);
}

View File

@ -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<Category, Integer> {
@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<Category, Integer> {
@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<Integer> affectedPostIdList);
/**
* List categories by parent id.
*
@ -109,34 +90,6 @@ public interface CategoryService extends CrudService<Category, Integer> {
*/
List<Category> 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<Category> listAll(Sort sort, boolean queryEncryptCategory);
/**
* List all category not encrypt.
*
* @param queryEncryptCategory whether to query encryption category
* @return list of category.
*/
List<Category> listAll(boolean queryEncryptCategory);
/**
* List all by ids
*
* @param ids ids
* @param queryEncryptCategory whether to query encryption category
* @return List
*/
@NonNull
List<Category> listAllByIds(Collection<Integer> ids, boolean queryEncryptCategory);
/**
* Converts to category dto.
*
@ -155,23 +108,23 @@ public interface CategoryService extends CrudService<Category, Integer> {
@NonNull
List<CategoryDTO> convertTo(@Nullable List<Category> categories);
/**
* Filter encrypt category
*
* @param categories this categories is not a category list tree
* @return category list
*/
@NonNull
List<Category> filterEncryptCategory(@Nullable List<Category> categories);
/**
* Determine whether the category is encrypted.
*
* @param categoryId category id
* @return whether to encrypt
*/
@NonNull
Boolean categoryHasEncrypt(Integer categoryId);
boolean isPrivate(Integer categoryId);
/**
* This method will first query all categories and create a tree, then start from the node
* whose ID is <code>categoryId</code> and recursively look up the first encryption category.
*
* @param categoryId categoryId to look up
* @return encrypted immediate parent category If it is found,otherwise an empty
* {@code Optional}.
*/
Optional<Category> lookupFirstEncryptedBy(Integer categoryId);
/**
* Use <code>categories</code> to build a category tree.

View File

@ -36,26 +36,14 @@ public interface PostCategoryService extends CrudService<PostCategory, Integer>
@NonNull
List<Category> 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<Category> 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<Integer, List<Category>> listCategoryListMap(
@Nullable Collection<Integer> postIds, @NonNull boolean queryEncryptCategory);
Map<Integer, List<Category>> listCategoryListMap(@Nullable Collection<Integer> postIds);
/**
* Lists post by category id.
@ -202,12 +190,10 @@ public interface PostCategoryService extends CrudService<PostCategory, Integer>
* Lists category with post count.
*
* @param sort sort info
* @param queryEncryptCategory whether to query encryption category
* @return a list of category dto
*/
@NonNull
List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(
@NonNull Sort sort, @NonNull boolean queryEncryptCategory);
List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(@NonNull Sort sort);
/**
* Lists by category id.

View File

@ -286,15 +286,6 @@ public interface PostService extends BasePostService<Post> {
@NonNull
List<PostListVO> convertToListVo(@NonNull List<Post> 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<PostListVO> convertToListVo(List<Post> posts, boolean queryEncryptCategory);
/**
* Publish a post visit event.
*

View File

@ -224,15 +224,6 @@ public interface BasePostService<POST extends BasePost> extends CrudService<POST
@NonNull
POST createOrUpdateBy(@NonNull POST post);
/**
* Filters post content if the password is not blank.
*
* @param post original post must not be null
* @return filtered post
*/
@NonNull
POST filterIfEncrypt(@NonNull POST post);
/**
* Convert POST to minimal dto.
*

View File

@ -1,110 +0,0 @@
package run.halo.app.service.impl;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import run.halo.app.model.entity.Category;
import run.halo.app.model.entity.Post;
import run.halo.app.repository.CategoryRepository;
import run.halo.app.repository.PostCategoryRepository;
import run.halo.app.service.AuthenticationService;
import run.halo.app.service.AuthorizationService;
/**
* @author ZhiXiang Yuan
* @date 2021/01/20 17:56
*/
@Service
public class AuthenticationServiceImpl implements AuthenticationService {
private final CategoryRepository categoryRepository;
private final AuthorizationService authorizationService;
private final PostCategoryRepository postCategoryRepository;
public AuthenticationServiceImpl(PostCategoryRepository postCategoryRepository,
CategoryRepository categoryRepository,
AuthorizationService authorizationService
) {
this.postCategoryRepository = postCategoryRepository;
this.categoryRepository = categoryRepository;
this.authorizationService = authorizationService;
}
@Override
public boolean postAuthentication(Post post, String password) {
Set<String> 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<Integer> 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<Integer, Category> idToCategoryMap = categoryRepository.findAll().stream()
.collect(Collectors.toMap(Category::getId, Function.identity()));
Set<String> accessPermissionStore = authorizationService.getAccessPermissionStore();
return doCategoryAuthentication(
idToCategoryMap, accessPermissionStore, categoryId, password);
}
private boolean doCategoryAuthentication(Map<Integer, Category> idToCategoryMap,
Set<String> 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);
}
}

View File

@ -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<String> 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<String> accessStore = getAccessPermissionStore();
accessStore.remove(value);
cacheStore.putAny(buildAccessPermissionKey(), accessStore, 1, TimeUnit.DAYS);
for (Entry<String, String> entry : cacheStore.toMap().entrySet()) {
String key = entry.getKey();
if (!key.startsWith(ACCESS_PERMISSION_PREFIX)) {
continue;
}
Set<String> valueSet = jsonToValueSet(entry.getValue());
if (valueSet.contains(value)) {
valueSet.remove(value);
cacheStore.putAny(key, valueSet, 1, TimeUnit.DAYS);
}
}
}
private Set<String> jsonToValueSet(String json) {
try {
return JsonUtils.DEFAULT_JSON_MAPPER.readValue(json,
new TypeReference<LinkedHashSet<String>>() {
});
} 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<String> 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();
}
}

View File

@ -348,7 +348,7 @@ public class BackupServiceImpl implements BackupService {
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());

View File

@ -329,23 +329,6 @@ public abstract class BasePostServiceImpl<POST extends BasePost>
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");

View File

@ -2,25 +2,20 @@ package run.halo.app.service.impl;
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.stereotype.Service;
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<Category, Integer>
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 class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
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<CategoryVO> listAsTree(Sort sort) {
Assert.notNull(sort, "Sort info must not be null");
@ -167,6 +151,11 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
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 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
return categoryVo;
}
@Override
public Category getBySlug(String slug) {
Optional<Category> 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<Category> 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 class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
}
// Remove category
removeById(categoryId);
Category category = removeById(categoryId);
// Remove post categories
List<Integer> affectedPostIdList = postCategoryService.removeByCategoryId(categoryId)
.stream().map(PostCategory::getPostId).collect(Collectors.toList());
refreshPostStatus(affectedPostIdList);
}
@Override
public void refreshPostStatus(List<Integer> 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 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
Queue<CategoryVO> 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 class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
}
@Override
public List<Category> filterEncryptCategory(List<Category> categories) {
if (CollectionUtils.isEmpty(categories)) {
return Collections.emptyList();
}
// list to tree, no password desensitise is required here
List<CategoryVO> categoryTree = listToTree(categories);
// filter encrypt category
doFilterEncryptCategory(categoryTree);
List<Category> 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<CategoryVO> 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<Category> collectorList,
List<CategoryVO> 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<Category> collectorList,
List<CategoryVO> 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<Category> listAll(Sort sort, boolean queryEncryptCategory) {
if (queryEncryptCategory) {
return super.listAll(sort);
} else {
return this.listAll(sort);
}
}
@Override
public List<Category> listAll(boolean queryEncryptCategory) {
if (queryEncryptCategory) {
return super.listAll();
} else {
return this.listAll();
}
}
@Override
public List<Category> listAll() {
return filterEncryptCategory(super.listAll());
}
@Override
public List<Category> listAll(Sort sort) {
return filterEncryptCategory(super.listAll(sort));
}
@Override
public Page<Category> 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<Category> listAllByIds(Collection<Integer> integers, boolean queryEncryptCategory) {
if (queryEncryptCategory) {
return super.listAllByIds(integers);
} else {
return this.listAllByIds(integers);
}
}
@Override
public List<Category> listAllByIds(Collection<Integer> integers) {
return filterEncryptCategory(super.listAllByIds(integers));
}
@Override
public List<Category> listAllByIds(Collection<Integer> 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<CategoryVO> categoryTree = listToTree(super.listAll());
List<Category> 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<Category> allCategoryList = super.listAll();
Map<Integer, Category> 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<CategoryVO> categoryTree = listToTree(allCategoryList);
List<Category> 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<Category> allCategoryList = super.listAll();
Map<Integer, Category> 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,29 +314,37 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
.collect(Collectors.toList());
}
@Override
public Optional<Category> lookupFirstEncryptedBy(Integer categoryId) {
List<Category> categories = listAll();
Map<Integer, Category> 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<Integer, Category> idToCategoryMap, Integer categoryId) {
if (categoryId == 0) {
return false;
}
private Category findFirstEncryptedCategoryBy(Map<Integer, Category> idToCategoryMap,
Integer categoryId) {
Category category = idToCategoryMap.get(categoryId);
if (StringUtils.isNotBlank(category.getPassword())) {
return true;
if (categoryId == 0 || category == null) {
return null;
}
return doCategoryHasEncrypt(idToCategoryMap, category.getParentId());
}
if (StringUtils.isNotBlank(category.getPassword())) {
return category;
}
return findFirstEncryptedCategoryBy(idToCategoryMap, category.getParentId());
}
@Override
@Transactional(rollbackFor = Exception.class)
@ -696,8 +360,12 @@ public class CategoryServiceImpl extends AbstractCrudService<Category, Integer>
.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());
}
}

View File

@ -76,23 +76,18 @@ public class PostCategoryServiceImpl extends AbstractCrudService<PostCategory, I
@Override
public List<Category> listCategoriesBy(Integer postId) {
return listCategoriesBy(postId, false);
}
@Override
public List<Category> listCategoriesBy(Integer postId, boolean queryEncryptCategory) {
Assert.notNull(postId, "Post id must not be null");
// Find all category ids
Set<Integer> categoryIds = postCategoryRepository.findAllCategoryIdsByPostId(postId);
return categoryService.listAllByIds(categoryIds, queryEncryptCategory);
return categoryService.listAllByIds(categoryIds);
}
@Override
public Map<Integer, List<Category>> listCategoryListMap(
Collection<Integer> postIds, boolean queryEncryptCategory) {
Collection<Integer> postIds) {
if (CollectionUtils.isEmpty(postIds)) {
return Collections.emptyMap();
}
@ -105,7 +100,7 @@ public class PostCategoryServiceImpl extends AbstractCrudService<PostCategory, I
ServiceUtils.fetchProperty(postCategories, PostCategory::getCategoryId);
// Find all categories
List<Category> categories = categoryService.listAllByIds(categoryIds, queryEncryptCategory);
List<Category> categories = categoryService.listAllByIds(categoryIds);
// Convert to category map
Map<Integer, Category> categoryMap = ServiceUtils.convertToMap(categories, Category::getId);
@ -309,10 +304,9 @@ public class PostCategoryServiceImpl extends AbstractCrudService<PostCategory, I
}
@Override
public List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(@NonNull Sort sort,
boolean queryEncryptCategory) {
public List<CategoryWithPostCountDTO> listCategoryWithPostCountDto(@NonNull Sort sort) {
Assert.notNull(sort, "Sort info must not be null");
List<Category> categories = categoryService.listAll(sort, queryEncryptCategory);
List<Category> categories = categoryService.listAll(sort);
List<CategoryVO> categoryTreeVo = categoryService.listToTree(categories);
populatePostIds(categoryTreeVo);

View File

@ -22,6 +22,7 @@ import javax.persistence.criteria.Subquery;
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.transaction.annotation.Transactional;
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.PostListVO;
import run.halo.app.model.vo.PostMarkdownVO;
import run.halo.app.repository.PostRepository;
import run.halo.app.repository.base.BasePostRepository;
import run.halo.app.service.AuthorizationService;
import run.halo.app.service.CategoryService;
import run.halo.app.service.ContentPatchLogService;
import run.halo.app.service.ContentService;
@ -113,10 +114,10 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
private final OptionService optionService;
private final AuthorizationService authorizationService;
private final ContentPatchLogService postContentPatchLogService;
private final ApplicationContext applicationContext;
public PostServiceImpl(BasePostRepository<Post> basePostRepository,
OptionService optionService,
PostRepository postRepository,
@ -127,9 +128,9 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
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 class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
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 class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
List<Tag> tags = postTagService.listTagsBy(post.getId());
// List categories
List<Category> categories = postCategoryService
.listCategoriesBy(post.getId(), queryEncryptCategory);
.listCategoriesBy(post.getId());
// List metas
List<PostMeta> metas = postMetaService.listBy(post.getId());
// Convert to detail vo
@ -633,7 +634,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
// Get category list map
Map<Integer, List<Category>> categoryListMap = postCategoryService
.listCategoryListMap(postIds, queryEncryptCategory);
.listCategoryListMap(postIds);
// Get comment count
Map<Integer, Long> commentCountMap = postCommentService.countByStatusAndPostIds(
@ -685,11 +686,6 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
@Override
public List<PostListVO> convertToListVo(List<Post> posts) {
return convertToListVo(posts, false);
}
@Override
public List<PostListVO> convertToListVo(List<Post> posts, boolean queryEncryptCategory) {
Assert.notNull(posts, "Post page must not be null");
Set<Integer> postIds = ServiceUtils.fetchProperty(posts, Post::getId);
@ -699,7 +695,7 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
// Get category list map
Map<Integer, List<Category>> categoryListMap = postCategoryService
.listCategoryListMap(postIds, queryEncryptCategory);
.listCategoryListMap(postIds);
// Get comment count
Map<Integer, Long> commentCountMap =
@ -885,21 +881,9 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
Set<Integer> categoryIds, Set<PostMeta> 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 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
List<Tag> tags = tagService.listAllByIds(tagIds);
// List all categories
List<Category> categories = categoryService.listAllByIds(categoryIds, true);
List<Category> categories = categoryService.listAllByIds(categoryIds);
// Create post tags
List<PostTag> postTags = postTagService.mergeOrCreateByIfAbsent(post.getId(),
@ -934,8 +918,8 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
.createOrUpdateByPostId(post.getId(), metas);
log.debug("Created post metas: [{}]", postMetaList);
// Remove authorization every time an post is created or updated.
authorizationService.deletePostAuthorization(post.getId());
// 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 @@ public class PostServiceImpl extends BasePostServiceImpl<Post> implements PostSe
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<Post> updateStatusByIds(List<Integer> 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));

View File

@ -160,6 +160,7 @@
<span class="top"></span>
<span class="left"></span>
</div>
<div style="margin-top: 8px;color: red;">${errorMsg!}</div>
<div class="submit-input">
<button type="submit">验证</button>
</div>

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -43,6 +43,34 @@ public class PostParamTest {
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<ConstraintViolation<PostParam>> 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();

View File

@ -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<String> permissions = authorizationService.getAccessPermissionStore();
assertEquals("[POST:1, POST:2]", permissions.toString());
authorizationService.deletePostAuthorization(1);
Set<String> 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;
}
}