mirror of https://github.com/halo-dev/halo
refactor: post and cateogry authentication (#1678)
parent
5064837cf8
commit
4f1a3c5f0b
|
@ -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")
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("您没有该分类的访问权限");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {};
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue