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
guqing 2024-09-13 10:14:25 +08:00 committed by GitHub
parent 07d200b45b
commit 7ed859cefb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 67 additions and 25 deletions

View File

@ -1,5 +1,6 @@
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.equal;
import static run.halo.app.extension.index.query.QueryFactory.isNull;
@ -8,8 +9,8 @@ import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.reactivestreams.Publisher;
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.ReactiveExtensionClient;
import run.halo.app.extension.router.selector.FieldSelector;
import run.halo.app.infra.exception.RequestRestrictedException;
import run.halo.app.metrics.CounterService;
/**
@ -40,6 +42,9 @@ import run.halo.app.metrics.CounterService;
@Service
public class ReplyServiceImpl extends AbstractCommentService implements ReplyService {
private final Supplier<RequestRestrictedException> requestRestrictedExceptionSupplier =
() -> new RequestRestrictedException("problemDetail.comment.waitingForApproval");
public ReplyServiceImpl(RoleService roleService, ReactiveExtensionClient client,
UserService userService, CounterService counterService) {
super(roleService, client, userService, counterService);
@ -48,26 +53,32 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
@Override
public Mono<Reply> create(String commentName, Reply reply) {
return client.get(Comment.class, commentName)
.flatMap(comment -> prepareReply(commentName, reply)
.flatMap(client::create)
.flatMap(createdReply -> {
var quotedReply = createdReply.getSpec().getQuoteReply();
if (StringUtils.isBlank(quotedReply)) {
return Mono.just(createdReply);
}
return approveReply(quotedReply)
.thenReturn(createdReply);
})
.flatMap(createdReply -> approveComment(comment)
.thenReturn(createdReply)
)
);
.flatMap(this::approveComment)
.filter(comment -> isTrue(comment.getSpec().getApproved()))
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
.flatMap(comment -> prepareReply(commentName, reply))
.flatMap(this::doCreateReply);
}
private Mono<Reply> doCreateReply(Reply prepared) {
var quotedReply = prepared.getSpec().getQuoteReply();
if (StringUtils.isBlank(quotedReply)) {
return client.create(prepared);
}
return approveReply(quotedReply)
.filter(reply -> isTrue(reply.getSpec().getApproved()))
.switchIfEmpty(Mono.error(requestRestrictedExceptionSupplier))
.flatMap(approvedQuoteReply -> client.create(prepared));
}
private Mono<Comment> approveComment(Comment comment) {
return hasCommentManagePermission()
.filter(Boolean::booleanValue)
.flatMap(hasPermission -> doApproveComment(comment));
.flatMap(hasPermission -> {
if (hasPermission) {
return doApproveComment(comment);
}
return Mono.just(comment);
});
}
private Mono<Comment> doApproveComment(Comment comment) {
@ -81,14 +92,18 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
e -> updateCommentWithRetry(comment.getMetadata().getName(), updateFunc));
}
private Mono<Void> approveReply(String replyName) {
private Mono<Reply> approveReply(String replyName) {
return hasCommentManagePermission()
.filter(Boolean::booleanValue)
.flatMap(hasPermission -> doApproveReply(replyName));
.flatMap(hasPermission -> {
if (hasPermission) {
return doApproveReply(replyName);
}
return client.get(Reply.class, replyName);
});
}
private Mono<Void> doApproveReply(String replyName) {
return Mono.defer(() -> client.fetch(Reply.class, replyName)
private Mono<Reply> doApproveReply(String replyName) {
return Mono.defer(() -> client.get(Reply.class, replyName)
.flatMap(reply -> {
reply.getSpec().setApproved(true);
reply.getSpec().setApprovedTime(Instant.now());
@ -96,8 +111,7 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer
})
)
.retryWhen(Retry.backoff(8, Duration.ofMillis(100))
.filter(OptimisticLockingFailureException.class::isInstance))
.then();
.filter(OptimisticLockingFailureException.class::isInstance));
}
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) {
reply.getSpec().setApproved(false);
}
if (BooleanUtils.isTrue(reply.getSpec().getApproved())
if (isTrue(reply.getSpec().getApproved())
&& reply.getSpec().getApprovedTime() == null) {
reply.getSpec().setApprovedTime(Instant.now());
}

View File

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

View File

@ -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.FileSizeExceededException=File Size Exceeded
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.run.halo.app.infra.exception.ThemeInstallationException=Theme Install 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.attachment.upload.fileSizeExceeded=Make sure the file size is less than {0}.
problemDetail.attachment.upload.fileTypeNotSupported=Unsupported upload of {0} type files.
problemDetail.comment.waitingForApproval=Comment is awaiting approval.
title.visibility.identification.private=(Private)

View File

@ -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.FileTypeNotAllowedException=文件类型不允许
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.PluginAlreadyExistsException=插件已存在
problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=主题安装失败
@ -51,5 +52,6 @@ problemDetail.conflict=检测到冲突,请检查数据后重试。
problemDetail.migration.backup.notFound=备份文件不存在或已删除。
problemDetail.attachment.upload.fileSizeExceeded=最大支持上传 {0} 大小的文件。
problemDetail.attachment.upload.fileTypeNotSupported=不支持上传 {0} 类型的文件。
problemDetail.comment.waitingForApproval=评论审核中。
title.visibility.identification.private=(私有)