From 7ed859cefb1165cd9789af1728a06cf272957fea Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Fri, 13 Sep 2024 10:14:25 +0800 Subject: [PATCH] refactor: prevent replies to comments that are pending approval (#6622) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### 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 禁止非管理员回复未通过审核的评论 ``` --- .../app/content/comment/ReplyServiceImpl.java | 64 +++++++++++-------- .../exception/RequestRestrictedException.java | 24 +++++++ .../resources/config/i18n/messages.properties | 2 + .../config/i18n/messages_zh.properties | 2 + 4 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 application/src/main/java/run/halo/app/infra/exception/RequestRestrictedException.java diff --git a/application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java b/application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java index a59bbd347..26b99e63e 100644 --- a/application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java +++ b/application/src/main/java/run/halo/app/content/comment/ReplyServiceImpl.java @@ -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 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 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 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 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 doApproveComment(Comment comment) { @@ -81,14 +92,18 @@ public class ReplyServiceImpl extends AbstractCommentService implements ReplySer e -> updateCommentWithRetry(comment.getMetadata().getName(), updateFunc)); } - private Mono approveReply(String replyName) { + private Mono 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 doApproveReply(String replyName) { - return Mono.defer(() -> client.fetch(Reply.class, replyName) + private Mono 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 updateCommentWithRetry(String name, UnaryOperator 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()); } diff --git a/application/src/main/java/run/halo/app/infra/exception/RequestRestrictedException.java b/application/src/main/java/run/halo/app/infra/exception/RequestRestrictedException.java new file mode 100644 index 000000000..1d86edffc --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/exception/RequestRestrictedException.java @@ -0,0 +1,24 @@ +package run.halo.app.infra.exception; + +/** + *

{@link RequestRestrictedException} indicates that the client's request was denied because + * it did not meet certain required conditions.

+ *

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.

+ *

The server understands the request but refuses to process it due to the lack of + * necessary approval.

+ * + * @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); + } +} diff --git a/application/src/main/resources/config/i18n/messages.properties b/application/src/main/resources/config/i18n/messages.properties index 8d1bf19bd..8a2e0dca0 100644 --- a/application/src/main/resources/config/i18n/messages.properties +++ b/application/src/main/resources/config/i18n/messages.properties @@ -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) \ No newline at end of file diff --git a/application/src/main/resources/config/i18n/messages_zh.properties b/application/src/main/resources/config/i18n/messages_zh.properties index 818f7e35e..fa5699ea0 100644 --- a/application/src/main/resources/config/i18n/messages_zh.properties +++ b/application/src/main/resources/config/i18n/messages_zh.properties @@ -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=(私有) \ No newline at end of file