mirror of https://github.com/halo-dev/halo
refactor: prevent replies to comments that are pending approval (#6622)
#### What type of PR is this? /kind improvement /area core /milestone 2.20.x #### What this PR does / why we need it: 不允许回复未通过审核的评论 #### Does this PR introduce a user-facing change? ```release-note 禁止非管理员回复未通过审核的评论 ```pull/6651/head
parent
07d200b45b
commit
7ed859cefb
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.content.comment;
|
package run.halo.app.content.comment;
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.BooleanUtils.isTrue;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.and;
|
import static run.halo.app.extension.index.query.QueryFactory.and;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
import static run.halo.app.extension.index.query.QueryFactory.equal;
|
||||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||||
|
@ -8,8 +9,8 @@ import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import java.util.function.UnaryOperator;
|
import java.util.function.UnaryOperator;
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import org.springframework.dao.OptimisticLockingFailureException;
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
|
@ -29,6 +30,7 @@ import run.halo.app.extension.PageRequest;
|
||||||
import run.halo.app.extension.PageRequestImpl;
|
import run.halo.app.extension.PageRequestImpl;
|
||||||
import run.halo.app.extension.ReactiveExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.router.selector.FieldSelector;
|
import run.halo.app.extension.router.selector.FieldSelector;
|
||||||
|
import run.halo.app.infra.exception.RequestRestrictedException;
|
||||||
import run.halo.app.metrics.CounterService;
|
import run.halo.app.metrics.CounterService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,6 +42,9 @@ import run.halo.app.metrics.CounterService;
|
||||||
@Service
|
@Service
|
||||||
public class ReplyServiceImpl extends AbstractCommentService implements ReplyService {
|
public class ReplyServiceImpl extends AbstractCommentService implements ReplyService {
|
||||||
|
|
||||||
|
private final Supplier<RequestRestrictedException> requestRestrictedExceptionSupplier =
|
||||||
|
() -> new RequestRestrictedException("problemDetail.comment.waitingForApproval");
|
||||||
|
|
||||||
public ReplyServiceImpl(RoleService roleService, ReactiveExtensionClient client,
|
public ReplyServiceImpl(RoleService roleService, ReactiveExtensionClient client,
|
||||||
UserService userService, CounterService counterService) {
|
UserService userService, CounterService counterService) {
|
||||||
super(roleService, client, userService, counterService);
|
super(roleService, client, userService, counterService);
|
||||||
|
@ -48,26 +53,32 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
|
||||||
@Override
|
@Override
|
||||||
public Mono<Reply> create(String commentName, Reply reply) {
|
public Mono<Reply> create(String commentName, Reply reply) {
|
||||||
return client.get(Comment.class, commentName)
|
return client.get(Comment.class, commentName)
|
||||||
.flatMap(comment -> prepareReply(commentName, reply)
|
.flatMap(this::approveComment)
|
||||||
.flatMap(client::create)
|
.filter(comment -> isTrue(comment.getSpec().getApproved()))
|
||||||
.flatMap(createdReply -> {
|
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
|
||||||
var quotedReply = createdReply.getSpec().getQuoteReply();
|
.flatMap(comment -> prepareReply(commentName, reply))
|
||||||
|
.flatMap(this::doCreateReply);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Reply> doCreateReply(Reply prepared) {
|
||||||
|
var quotedReply = prepared.getSpec().getQuoteReply();
|
||||||
if (StringUtils.isBlank(quotedReply)) {
|
if (StringUtils.isBlank(quotedReply)) {
|
||||||
return Mono.just(createdReply);
|
return client.create(prepared);
|
||||||
}
|
}
|
||||||
return approveReply(quotedReply)
|
return approveReply(quotedReply)
|
||||||
.thenReturn(createdReply);
|
.filter(reply -> isTrue(reply.getSpec().getApproved()))
|
||||||
})
|
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
|
||||||
.flatMap(createdReply -> approveComment(comment)
|
.flatMap(approvedQuoteReply -> client.create(prepared));
|
||||||
.thenReturn(createdReply)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Comment> approveComment(Comment comment) {
|
private Mono<Comment> approveComment(Comment comment) {
|
||||||
return hasCommentManagePermission()
|
return hasCommentManagePermission()
|
||||||
.filter(Boolean::booleanValue)
|
.flatMap(hasPermission -> {
|
||||||
.flatMap(hasPermission -> doApproveComment(comment));
|
if (hasPermission) {
|
||||||
|
return doApproveComment(comment);
|
||||||
|
}
|
||||||
|
return Mono.just(comment);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Comment> doApproveComment(Comment comment) {
|
private Mono<Comment> doApproveComment(Comment comment) {
|
||||||
|
@ -81,14 +92,18 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
|
||||||
e -> updateCommentWithRetry(comment.getMetadata().getName(), updateFunc));
|
e -> updateCommentWithRetry(comment.getMetadata().getName(), updateFunc));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Void> approveReply(String replyName) {
|
private Mono<Reply> approveReply(String replyName) {
|
||||||
return hasCommentManagePermission()
|
return hasCommentManagePermission()
|
||||||
.filter(Boolean::booleanValue)
|
.flatMap(hasPermission -> {
|
||||||
.flatMap(hasPermission -> doApproveReply(replyName));
|
if (hasPermission) {
|
||||||
|
return doApproveReply(replyName);
|
||||||
|
}
|
||||||
|
return client.get(Reply.class, replyName);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Void> doApproveReply(String replyName) {
|
private Mono<Reply> doApproveReply(String replyName) {
|
||||||
return Mono.defer(() -> client.fetch(Reply.class, replyName)
|
return Mono.defer(() -> client.get(Reply.class, replyName)
|
||||||
.flatMap(reply -> {
|
.flatMap(reply -> {
|
||||||
reply.getSpec().setApproved(true);
|
reply.getSpec().setApproved(true);
|
||||||
reply.getSpec().setApprovedTime(Instant.now());
|
reply.getSpec().setApprovedTime(Instant.now());
|
||||||
|
@ -96,8 +111,7 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
|
||||||
.filter(OptimisticLockingFailureException.class::isInstance))
|
.filter(OptimisticLockingFailureException.class::isInstance));
|
||||||
.then();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<Comment> updateCommentWithRetry(String name, UnaryOperator<Comment> updateFunc) {
|
private Mono<Comment> updateCommentWithRetry(String name, UnaryOperator<Comment> updateFunc) {
|
||||||
|
@ -123,7 +137,7 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
|
||||||
if (reply.getSpec().getApproved() == null) {
|
if (reply.getSpec().getApproved() == null) {
|
||||||
reply.getSpec().setApproved(false);
|
reply.getSpec().setApproved(false);
|
||||||
}
|
}
|
||||||
if (BooleanUtils.isTrue(reply.getSpec().getApproved())
|
if (isTrue(reply.getSpec().getApproved())
|
||||||
&& reply.getSpec().getApprovedTime() == null) {
|
&& reply.getSpec().getApprovedTime() == null) {
|
||||||
reply.getSpec().setApprovedTime(Instant.now());
|
reply.getSpec().setApprovedTime(Instant.now());
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package run.halo.app.infra.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>{@link RequestRestrictedException} indicates that the client's request was denied because
|
||||||
|
* it did not meet certain required conditions.</p>
|
||||||
|
* <p>Typically, this exception is thrown when a user attempts to perform an action that
|
||||||
|
* requires prior approval or validation, such as replying to a comment that has not yet been
|
||||||
|
* approved.</p>
|
||||||
|
* <p>The server understands the request but refuses to process it due to the lack of
|
||||||
|
* necessary approval.</p>
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.20.0
|
||||||
|
*/
|
||||||
|
public class RequestRestrictedException extends AccessDeniedException {
|
||||||
|
|
||||||
|
public RequestRestrictedException(String reason) {
|
||||||
|
super(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestRestrictedException(String reason, String detailCode, Object[] detailArgs) {
|
||||||
|
super(reason, detailCode, detailArgs);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ problemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsExceptio
|
||||||
problemDetail.title.run.halo.app.infra.exception.FileTypeNotAllowedException=File Type Not Allowed
|
problemDetail.title.run.halo.app.infra.exception.FileTypeNotAllowedException=File Type Not Allowed
|
||||||
problemDetail.title.run.halo.app.infra.exception.FileSizeExceededException=File Size Exceeded
|
problemDetail.title.run.halo.app.infra.exception.FileSizeExceededException=File Size Exceeded
|
||||||
problemDetail.title.run.halo.app.infra.exception.AccessDeniedException=Access Denied
|
problemDetail.title.run.halo.app.infra.exception.AccessDeniedException=Access Denied
|
||||||
|
problemDetail.title.run.halo.app.infra.exception.RequestRestrictedException=Request Restricted
|
||||||
problemDetail.title.reactor.core.Exceptions.RetryExhaustedException=Retry Exhausted
|
problemDetail.title.reactor.core.Exceptions.RetryExhaustedException=Retry Exhausted
|
||||||
problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=Theme Install Error
|
problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=Theme Install Error
|
||||||
problemDetail.title.run.halo.app.infra.exception.ThemeUpgradeException=Theme Upgrade Error
|
problemDetail.title.run.halo.app.infra.exception.ThemeUpgradeException=Theme Upgrade Error
|
||||||
|
@ -79,5 +80,6 @@ problemDetail.conflict=Conflict detected, please check the data and retry.
|
||||||
problemDetail.migration.backup.notFound=The backup file does not exist or has been deleted.
|
problemDetail.migration.backup.notFound=The backup file does not exist or has been deleted.
|
||||||
problemDetail.attachment.upload.fileSizeExceeded=Make sure the file size is less than {0}.
|
problemDetail.attachment.upload.fileSizeExceeded=Make sure the file size is less than {0}.
|
||||||
problemDetail.attachment.upload.fileTypeNotSupported=Unsupported upload of {0} type files.
|
problemDetail.attachment.upload.fileTypeNotSupported=Unsupported upload of {0} type files.
|
||||||
|
problemDetail.comment.waitingForApproval=Comment is awaiting approval.
|
||||||
|
|
||||||
title.visibility.identification.private=(Private)
|
title.visibility.identification.private=(Private)
|
|
@ -5,6 +5,7 @@ problemDetail.title.run.halo.app.infra.exception.PluginInstallationException=插
|
||||||
problemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsException=附件已存在
|
problemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsException=附件已存在
|
||||||
problemDetail.title.run.halo.app.infra.exception.FileTypeNotAllowedException=文件类型不允许
|
problemDetail.title.run.halo.app.infra.exception.FileTypeNotAllowedException=文件类型不允许
|
||||||
problemDetail.title.run.halo.app.infra.exception.FileSizeExceededException=文件大小超出限制
|
problemDetail.title.run.halo.app.infra.exception.FileSizeExceededException=文件大小超出限制
|
||||||
|
problemDetail.title.run.halo.app.infra.exception.RequestRestrictedException=请求受限
|
||||||
problemDetail.title.run.halo.app.infra.exception.DuplicateNameException=名称重复
|
problemDetail.title.run.halo.app.infra.exception.DuplicateNameException=名称重复
|
||||||
problemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=插件已存在
|
problemDetail.title.run.halo.app.infra.exception.PluginAlreadyExistsException=插件已存在
|
||||||
problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=主题安装失败
|
problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=主题安装失败
|
||||||
|
@ -51,5 +52,6 @@ problemDetail.conflict=检测到冲突,请检查数据后重试。
|
||||||
problemDetail.migration.backup.notFound=备份文件不存在或已删除。
|
problemDetail.migration.backup.notFound=备份文件不存在或已删除。
|
||||||
problemDetail.attachment.upload.fileSizeExceeded=最大支持上传 {0} 大小的文件。
|
problemDetail.attachment.upload.fileSizeExceeded=最大支持上传 {0} 大小的文件。
|
||||||
problemDetail.attachment.upload.fileTypeNotSupported=不支持上传 {0} 类型的文件。
|
problemDetail.attachment.upload.fileTypeNotSupported=不支持上传 {0} 类型的文件。
|
||||||
|
problemDetail.comment.waitingForApproval=评论审核中。
|
||||||
|
|
||||||
title.visibility.identification.private=(私有)
|
title.visibility.identification.private=(私有)
|
Loading…
Reference in New Issue