From 637071b2605a23ffca6ba5ea16c8f3379b604a5a Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Fri, 25 Aug 2023 22:12:12 +0800 Subject: [PATCH] feat: support displaying private posts for owner on theme-side (#4412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /milestone 2.9.x #### What this PR does / why we need it: 登录后支持在主题端展示作者的私有文章 how to test it? 1. 测试登录后是否能访问到自己创建的私有文章,退出登录后私有文章消失 2. 不能在在主题端看到别人创建的私有文章 3. 创建私有文章测试登录后使用主题端的上一页下一页功能是否正常 #### Which issue(s) this PR fixes: Fixes #3016 #### Does this PR introduce a user-facing change? ```release-note 登录后支持在主题端展示作者的私有文章 ``` --- .../theme/finders/PostPublicQueryService.java | 5 -- .../theme/finders/impl/PostFinderImpl.java | 24 +++-- .../impl/PostPublicQueryServiceImpl.java | 17 ++-- .../finders/impl/SinglePageFinderImpl.java | 25 +++++- .../DefaultQueryPostPredicateResolver.java | 43 +++++++++ .../ReactiveQueryPostPredicateResolver.java | 16 ++++ .../app/theme/router/SinglePageRoute.java | 13 +++ .../TitleVisibilityIdentifyCalculator.java | 34 ++++++++ .../router/factories/ArchiveRouteFactory.java | 19 ++++ .../factories/AuthorPostsRouteFactory.java | 17 ++++ .../factories/CategoryPostRouteFactory.java | 15 ++++ .../router/factories/IndexRouteFactory.java | 15 ++++ .../router/factories/PostRouteFactory.java | 50 ++++++----- .../router/factories/TagPostRouteFactory.java | 18 +++- .../resources/config/i18n/messages.properties | 2 + .../config/i18n/messages_zh.properties | 2 + .../finders/impl/PostFinderImplTest.java | 8 +- ...eactiveQueryPostPredicateResolverTest.java | 87 +++++++++++++++++++ .../app/theme/router/SinglePageRouteTest.java | 11 +++ .../factories/PostRouteFactoryTest.java | 19 ++++ 20 files changed, 396 insertions(+), 44 deletions(-) create mode 100644 application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java create mode 100644 application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java create mode 100644 application/src/main/java/run/halo/app/theme/router/TitleVisibilityIdentifyCalculator.java create mode 100644 application/src/test/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolverTest.java diff --git a/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java b/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java index b2417fa8e..92c4e5f90 100644 --- a/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java +++ b/application/src/main/java/run/halo/app/theme/finders/PostPublicQueryService.java @@ -1,7 +1,6 @@ package run.halo.app.theme.finders; import java.util.Comparator; -import java.util.Objects; import java.util.function.Predicate; import org.springframework.lang.NonNull; import reactor.core.publisher.Mono; @@ -13,10 +12,6 @@ import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.PostVo; public interface PostPublicQueryService { - Predicate FIXED_PREDICATE = post -> post.isPublished() - && Objects.equals(false, post.getSpec().getDeleted()) - && Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible()); - /** * Lists posts page by predicate and comparator. diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java index db3757b6b..09b5c312a 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/PostFinderImpl.java @@ -1,7 +1,5 @@ package run.halo.app.theme.finders.impl; -import static run.halo.app.theme.finders.PostPublicQueryService.FIXED_PREDICATE; - import java.time.Instant; import java.util.ArrayDeque; import java.util.ArrayList; @@ -32,6 +30,7 @@ import run.halo.app.theme.finders.vo.NavigationPostVo; import run.halo.app.theme.finders.vo.PostArchiveVo; import run.halo.app.theme.finders.vo.PostArchiveYearMonthVo; import run.halo.app.theme.finders.vo.PostVo; +import run.halo.app.theme.router.ReactiveQueryPostPredicateResolver; /** * A finder for {@link Post}. @@ -43,17 +42,20 @@ import run.halo.app.theme.finders.vo.PostVo; @AllArgsConstructor public class PostFinderImpl implements PostFinder { - private final ReactiveExtensionClient client; private final PostPublicQueryService postPublicQueryService; + private final ReactiveQueryPostPredicateResolver postPredicateResolver; + @Override public Mono getByName(String postName) { - return client.get(Post.class, postName) - .filter(FIXED_PREDICATE) - .flatMap(post -> postPublicQueryService.convertToVo(post, - post.getSpec().getReleaseSnapshot()) + return postPredicateResolver.getPredicate() + .flatMap(predicate -> client.get(Post.class, postName) + .filter(predicate) + .flatMap(post -> postPublicQueryService.convertToVo(post, + post.getSpec().getReleaseSnapshot()) + ) ); } @@ -65,7 +67,10 @@ public class PostFinderImpl implements PostFinder { @Override public Mono cursor(String currentName) { // TODO Optimize the post names query here - return client.list(Post.class, FIXED_PREDICATE, defaultComparator()) + return postPredicateResolver.getPredicate() + .flatMapMany(postPredicate -> + client.list(Post.class, postPredicate, defaultComparator()) + ) .map(post -> post.getMetadata().getName()) .collectList() .flatMap(postNames -> Mono.just(NavigationPostVo.builder()) @@ -98,7 +103,8 @@ public class PostFinderImpl implements PostFinder { @Override public Flux listAll() { - return client.list(Post.class, FIXED_PREDICATE, defaultComparator()) + return postPredicateResolver.getPredicate() + .flatMapMany(predicate -> client.list(Post.class, predicate, defaultComparator())) .concatMap(postPublicQueryService::convertToListedVo); } diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java index e3065dfd1..5bc024a4b 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/PostPublicQueryServiceImpl.java @@ -29,6 +29,7 @@ import run.halo.app.theme.finders.vo.ContentVo; import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.finders.vo.StatsVo; +import run.halo.app.theme.router.ReactiveQueryPostPredicateResolver; @Component @RequiredArgsConstructor @@ -48,13 +49,16 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService { private final ExtensionGetter extensionGetter; + private final ReactiveQueryPostPredicateResolver postPredicateResolver; + @Override public Mono> list(Integer page, Integer size, Predicate postPredicate, Comparator comparator) { - Predicate predicate = FIXED_PREDICATE - .and(postPredicate == null ? post -> true : postPredicate); - return client.list(Post.class, predicate, + return postPredicateResolver.getPredicate() + .map(predicate -> predicate.and(postPredicate == null ? post -> true : postPredicate)) + .flatMap(predicate -> client.list(Post.class, predicate, comparator, pageNullSafe(page), sizeNullSafe(size)) + ) .flatMap(list -> Flux.fromStream(list.get()) .concatMap(post -> convertToListedVo(post) .flatMap(postVo -> populateStats(postVo) @@ -118,7 +122,6 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService { @Override public Mono convertToVo(Post post, String snapshotName) { - final String postName = post.getMetadata().getName(); final String baseSnapshotName = post.getSpec().getBaseSnapshot(); return convertToListedVo(post) .map(PostVo::from) @@ -131,8 +134,10 @@ public class PostPublicQueryServiceImpl implements PostPublicQueryService { @Override public Mono getContent(String postName) { - return client.get(Post.class, postName) - .filter(FIXED_PREDICATE) + return postPredicateResolver.getPredicate() + .flatMap(predicate -> client.get(Post.class, postName) + .filter(predicate) + ) .flatMap(post -> { String releaseSnapshot = post.getSpec().getReleaseSnapshot(); return postService.getContent(releaseSnapshot, post.getSpec().getBaseSnapshot()) diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java index af97747d0..256e7ef81 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/SinglePageFinderImpl.java @@ -1,5 +1,6 @@ package run.halo.app.theme.finders.impl; +import java.security.Principal; import java.time.Instant; import java.util.Comparator; import java.util.List; @@ -10,12 +11,15 @@ import java.util.function.Predicate; import lombok.AllArgsConstructor; import org.apache.commons.lang3.ObjectUtils; import org.springframework.lang.Nullable; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Post; import run.halo.app.core.extension.content.SinglePage; import run.halo.app.extension.ListResult; import run.halo.app.extension.ReactiveExtensionClient; +import run.halo.app.infra.AnonymousUserConst; import run.halo.app.theme.finders.Finder; import run.halo.app.theme.finders.SinglePageConversionService; import run.halo.app.theme.finders.SinglePageFinder; @@ -44,7 +48,7 @@ public class SinglePageFinderImpl implements SinglePageFinder { @Override public Mono getByName(String pageName) { return client.get(SinglePage.class, pageName) - .filter(FIXED_PREDICATE) + .filterWhen(page -> queryPredicate().map(predicate -> predicate.test(page))) .flatMap(singlePagePublicQueryService::convertToVo); } @@ -78,6 +82,25 @@ public class SinglePageFinderImpl implements SinglePageFinder { .defaultIfEmpty(new ListResult<>(0, 0, 0, List.of())); } + Mono> queryPredicate() { + Predicate predicate = page -> page.isPublished() + && Objects.equals(false, page.getSpec().getDeleted()); + Predicate visiblePredicate = + page -> Post.VisibleEnum.PUBLIC.equals(page.getSpec().getVisible()); + return currentUserName() + .map(username -> predicate.and( + visiblePredicate.or(page -> username.equals(page.getSpec().getOwner()))) + ) + .defaultIfEmpty(predicate.and(visiblePredicate)); + } + + Mono currentUserName() { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(Principal::getName) + .filter(name -> !AnonymousUserConst.isAnonymousUser(name)); + } + static Comparator defaultComparator() { Function pinned = page -> Objects.requireNonNullElse(page.getSpec().getPinned(), false); diff --git a/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java b/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java new file mode 100644 index 000000000..d111014f0 --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/router/DefaultQueryPostPredicateResolver.java @@ -0,0 +1,43 @@ +package run.halo.app.theme.router; + +import java.security.Principal; +import java.util.Objects; +import java.util.function.Predicate; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.ExtensionUtil; +import run.halo.app.infra.AnonymousUserConst; + +/** + * The default implementation of {@link ReactiveQueryPostPredicateResolver}. + * + * @author guqing + * @since 2.9.0 + */ +@Component +public class DefaultQueryPostPredicateResolver implements ReactiveQueryPostPredicateResolver { + + @Override + public Mono> getPredicate() { + Predicate predicate = post -> post.isPublished() + && !ExtensionUtil.isDeleted(post) + && Objects.equals(false, post.getSpec().getDeleted()); + Predicate visiblePredicate = + post -> Post.VisibleEnum.PUBLIC.equals(post.getSpec().getVisible()); + return currentUserName() + .map(username -> predicate.and( + visiblePredicate.or(post -> username.equals(post.getSpec().getOwner()))) + ) + .defaultIfEmpty(predicate.and(visiblePredicate)); + } + + Mono currentUserName() { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(Principal::getName) + .filter(name -> !AnonymousUserConst.isAnonymousUser(name)); + } +} diff --git a/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java b/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java new file mode 100644 index 000000000..ee754dc60 --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolver.java @@ -0,0 +1,16 @@ +package run.halo.app.theme.router; + +import java.util.function.Predicate; +import reactor.core.publisher.Mono; +import run.halo.app.core.extension.content.Post; + +/** + * The reactive query post predicate resolver. + * + * @author guqing + * @since 2.9.0 + */ +public interface ReactiveQueryPostPredicateResolver { + + Mono> getPredicate(); +} diff --git a/application/src/main/java/run/halo/app/theme/router/SinglePageRoute.java b/application/src/main/java/run/halo/app/theme/router/SinglePageRoute.java index 068f11541..c48bb15d0 100644 --- a/application/src/main/java/run/halo/app/theme/router/SinglePageRoute.java +++ b/application/src/main/java/run/halo/app/theme/router/SinglePageRoute.java @@ -23,6 +23,7 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.i18n.LocaleContextResolver; import org.springframework.web.util.UriUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -55,6 +56,10 @@ public class SinglePageRoute private final ViewNameResolver viewNameResolver; + private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + + private final LocaleContextResolver localeContextResolver; + @Override @NonNull public Mono> route(@NonNull ServerRequest request) { @@ -144,6 +149,14 @@ public class SinglePageRoute HandlerFunction handlerFunction(String name) { return request -> singlePageFinder.getByName(name) + .doOnNext(singlePageVo -> { + titleVisibilityIdentifyCalculator.calculateTitle( + singlePageVo.getSpec().getTitle(), + singlePageVo.getSpec().getVisible(), + localeContextResolver.resolveLocaleContext(request.exchange()) + .getLocale() + ); + }) .flatMap(singlePageVo -> { Map model = ModelMapUtils.singlePageModel(singlePageVo); String template = singlePageVo.getSpec().getTemplate(); diff --git a/application/src/main/java/run/halo/app/theme/router/TitleVisibilityIdentifyCalculator.java b/application/src/main/java/run/halo/app/theme/router/TitleVisibilityIdentifyCalculator.java new file mode 100644 index 000000000..812444583 --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/router/TitleVisibilityIdentifyCalculator.java @@ -0,0 +1,34 @@ +package run.halo.app.theme.router; + +import java.util.Locale; +import lombok.AllArgsConstructor; +import org.springframework.context.MessageSource; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import run.halo.app.core.extension.content.Post; + +@Component +@AllArgsConstructor +public class TitleVisibilityIdentifyCalculator { + + private final MessageSource messageSource; + + /** + * Calculate title with visibility identification. + * + * @param title title must not be null + * @param visibleEnum visibility enum + */ + public String calculateTitle(String title, Post.VisibleEnum visibleEnum, Locale locale) { + Assert.notNull(title, "Title must not be null"); + if (Post.VisibleEnum.PRIVATE.equals(visibleEnum)) { + String identify = messageSource.getMessage( + "title.visibility.identification.private", + null, + "", + locale); + return title + identify; + } + return title; + } +} diff --git a/application/src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java index 3a92c4569..a931524a9 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/ArchiveRouteFactory.java @@ -17,6 +17,7 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.i18n.LocaleContextResolver; import reactor.core.publisher.Mono; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; @@ -27,6 +28,7 @@ import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.vo.PostArchiveVo; import run.halo.app.theme.router.ModelConst; import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.UrlContextListResult; /** @@ -44,6 +46,10 @@ public class ArchiveRouteFactory implements RouteFactory { private final SystemConfigurableEnvironmentFetcher environmentFetcher; + private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + + private final LocaleContextResolver localeContextResolver; + @Override public RouterFunction create(String prefix) { RequestPredicate requestPredicate = patterns(prefix).stream() @@ -83,6 +89,19 @@ public class ArchiveRouteFactory implements RouteFactory { return configuredPageSize(environmentFetcher, SystemSetting.Post::getArchivePageSize) .flatMap(pageSize -> postFinder.archives(pageNum, pageSize, variables.getYear(), variables.getMonth())) + .doOnNext(list -> list.get() + .map(PostArchiveVo::getMonths) + .flatMap(List::stream) + .flatMap(month -> month.getPosts().stream()) + .forEach(postVo -> postVo.getSpec() + .setTitle(titleVisibilityIdentifyCalculator.calculateTitle( + postVo.getSpec().getTitle(), + postVo.getSpec().getVisible(), + localeContextResolver.resolveLocaleContext(request.exchange()) + .getLocale()) + ) + ) + ) .map(list -> new UrlContextListResult.Builder() .listResult(list) .nextUrl(PageUrlUtils.nextPageUrl(requestPath, totalPage(list))) diff --git a/application/src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java index 481759182..31fe7200b 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/AuthorPostsRouteFactory.java @@ -13,6 +13,7 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.i18n.LocaleContextResolver; import reactor.core.publisher.Mono; import run.halo.app.core.extension.User; import run.halo.app.extension.ReactiveExtensionClient; @@ -25,6 +26,7 @@ import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.UserVo; import run.halo.app.theme.router.ModelConst; import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.UrlContextListResult; /** @@ -42,6 +44,10 @@ public class AuthorPostsRouteFactory implements RouteFactory { private final ReactiveExtensionClient client; private SystemConfigurableEnvironmentFetcher environmentFetcher; + private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + + private final LocaleContextResolver localeContextResolver; + @Override public RouterFunction create(String pattern) { return RouterFunctions @@ -67,6 +73,17 @@ public class AuthorPostsRouteFactory implements RouteFactory { int pageNum = pageNumInPathVariable(request); return configuredPageSize(environmentFetcher, SystemSetting.Post::getPostPageSize) .flatMap(pageSize -> postFinder.listByOwner(pageNum, pageSize, name)) + .doOnNext(list -> { + list.getItems().forEach(listedPostVo -> { + listedPostVo.getSpec().setTitle( + titleVisibilityIdentifyCalculator.calculateTitle( + listedPostVo.getSpec().getTitle(), + listedPostVo.getSpec().getVisible(), + localeContextResolver.resolveLocaleContext(request.exchange()) + .getLocale()) + ); + }); + }) .map(list -> new UrlContextListResult.Builder() .listResult(list) .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) diff --git a/application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java index 734451bbe..02edd2c36 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/CategoryPostRouteFactory.java @@ -14,6 +14,7 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.i18n.LocaleContextResolver; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Category; import run.halo.app.extension.ReactiveExtensionClient; @@ -27,6 +28,7 @@ import run.halo.app.theme.finders.vo.CategoryVo; import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.router.ModelConst; import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.UrlContextListResult; import run.halo.app.theme.router.ViewNameResolver; @@ -47,6 +49,10 @@ public class CategoryPostRouteFactory implements RouteFactory { private final ReactiveExtensionClient client; private final ViewNameResolver viewNameResolver; + private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + + private final LocaleContextResolver localeContextResolver; + @Override public RouterFunction create(String prefix) { return RouterFunctions.route(GET(PathUtils.combinePath(prefix, "/{slug}")) @@ -87,6 +93,15 @@ public class CategoryPostRouteFactory implements RouteFactory { int pageNum = pageNumInPathVariable(request); return configuredPageSize(environmentFetcher, SystemSetting.Post::getCategoryPageSize) .flatMap(pageSize -> postFinder.listByCategory(pageNum, pageSize, name)) + .doOnNext(list -> list.forEach(postVo -> postVo.getSpec().setTitle( + titleVisibilityIdentifyCalculator.calculateTitle( + postVo.getSpec().getTitle(), + postVo.getSpec().getVisible(), + localeContextResolver.resolveLocaleContext(request.exchange()) + .getLocale() + ) + ) + )) .map(list -> new UrlContextListResult.Builder() .listResult(list) .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) diff --git a/application/src/main/java/run/halo/app/theme/router/factories/IndexRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/IndexRouteFactory.java index 09ee7b247..eecaedbbc 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/IndexRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/IndexRouteFactory.java @@ -13,6 +13,7 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.i18n.LocaleContextResolver; import reactor.core.publisher.Mono; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; @@ -21,6 +22,7 @@ import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.router.ModelConst; import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.UrlContextListResult; /** @@ -36,6 +38,8 @@ public class IndexRouteFactory implements RouteFactory { private final PostFinder postFinder; private final SystemConfigurableEnvironmentFetcher environmentFetcher; + private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + private final LocaleContextResolver localeContextResolver; @Override public RouterFunction create(String pattern) { @@ -54,8 +58,19 @@ public class IndexRouteFactory implements RouteFactory { private Mono> postList(ServerRequest request) { String path = request.path(); + return configuredPageSize(environmentFetcher, SystemSetting.Post::getPostPageSize) .flatMap(pageSize -> postFinder.list(pageNumInPathVariable(request), pageSize)) + .doOnNext(list -> list.getItems() + .forEach(listedPostVo -> listedPostVo.getSpec() + .setTitle(titleVisibilityIdentifyCalculator.calculateTitle( + listedPostVo.getSpec().getTitle(), + listedPostVo.getSpec().getVisible(), + localeContextResolver.resolveLocaleContext(request.exchange()) + .getLocale()) + ) + ) + ) .map(list -> new UrlContextListResult.Builder() .listResult(list) .nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list))) diff --git a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java index 6acf6227e..5dd474be0 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java @@ -2,7 +2,6 @@ package run.halo.app.theme.router.factories; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; import static org.springframework.web.reactive.function.server.RequestPredicates.accept; -import static run.halo.app.theme.finders.PostPublicQueryService.FIXED_PREDICATE; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -14,6 +13,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Getter; import org.apache.commons.lang3.StringUtils; import org.springframework.http.MediaType; import org.springframework.lang.NonNull; @@ -26,6 +26,7 @@ import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.i18n.LocaleContextResolver; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Post; @@ -37,6 +38,8 @@ import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.router.ModelMapUtils; +import run.halo.app.theme.router.ReactiveQueryPostPredicateResolver; +import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.ViewNameResolver; /** @@ -56,6 +59,12 @@ public class PostRouteFactory implements RouteFactory { private final ReactiveExtensionClient client; + private final ReactiveQueryPostPredicateResolver queryPostPredicateResolver; + + private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + + private final LocaleContextResolver localeContextResolver; + @Override public RouterFunction create(String pattern) { PatternParser postParamPredicate = @@ -73,7 +82,7 @@ public class PostRouteFactory implements RouteFactory { return request -> { Map variables = mergedVariables(request); PostPatternVariable patternVariable = new PostPatternVariable(); - Optional.ofNullable(variables.get(paramPredicate.getQueryParamName())) + Optional.ofNullable(variables.get(paramPredicate.getParamName())) .ifPresent(value -> { switch (paramPredicate.getPlaceholderName()) { case "name" -> patternVariable.setName(value); @@ -98,6 +107,15 @@ public class PostRouteFactory implements RouteFactory { PostPatternVariable patternVariable) { Mono postVoMono = bestMatchPost(patternVariable); return postVoMono + .doOnNext(postVo -> { + postVo.getSpec().setTitle( + titleVisibilityIdentifyCalculator.calculateTitle( + postVo.getSpec().getTitle(), + postVo.getSpec().getVisible(), + localeContextResolver.resolveLocaleContext(request.exchange()) + .getLocale()) + ); + }) .flatMap(postVo -> { Map model = ModelMapUtils.postModel(postVo); String template = postVo.getSpec().getTemplate(); @@ -133,16 +151,19 @@ public class PostRouteFactory implements RouteFactory { } private Flux fetchPostsByName(String name) { - return client.fetch(Post.class, name) - .filter(FIXED_PREDICATE) + return queryPostPredicateResolver.getPredicate() + .flatMap(predicate -> client.fetch(Post.class, name) + .filter(predicate) + ) .flux(); } private Flux fetchPostsBySlug(String slug) { - return client.list(Post.class, - post -> FIXED_PREDICATE.test(post) - && matchIfPresent(slug, post.getSpec().getSlug()), - null); + return queryPostPredicateResolver.getPredicate() + .flatMapMany(predicate -> client.list(Post.class, + predicate.and(post -> matchIfPresent(slug, post.getSpec().getSlug())), + null) + ); } private boolean matchIfPresent(String variable, String target) { @@ -175,6 +196,7 @@ public class PostRouteFactory implements RouteFactory { return mergedVariables; } + @Getter static class PatternParser { private static final Pattern PATTERN_COMPILE = Pattern.compile("([^&?]*)=\\{(.*?)\\}(&|$)"); private static final Cache MATCHER_CACHE = CacheBuilder.newBuilder() @@ -213,17 +235,5 @@ public class PostRouteFactory implements RouteFactory { return RequestPredicates.queryParam(paramName, value -> true); } - - public String getPlaceholderName() { - return this.placeholderName; - } - - public String getQueryParamName() { - return this.paramName; - } - - public boolean isQueryParamPattern() { - return isQueryParamPattern; - } } } diff --git a/application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java index 097ecc413..1257030eb 100644 --- a/application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java +++ b/application/src/main/java/run/halo/app/theme/router/factories/TagPostRouteFactory.java @@ -12,6 +12,7 @@ import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.i18n.LocaleContextResolver; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.Tag; import run.halo.app.extension.ReactiveExtensionClient; @@ -25,6 +26,7 @@ import run.halo.app.theme.finders.TagFinder; import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.TagVo; import run.halo.app.theme.router.PageUrlUtils; +import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.UrlContextListResult; /** @@ -43,6 +45,10 @@ public class TagPostRouteFactory implements RouteFactory { private final TagFinder tagFinder; private final PostFinder postFinder; + private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + + private final LocaleContextResolver localeContextResolver; + @Override public RouterFunction create(String prefix) { return RouterFunctions @@ -56,7 +62,17 @@ public class TagPostRouteFactory implements RouteFactory { .flatMap(tagVo -> { int pageNum = pageNumInPathVariable(request); String path = request.path(); - var postList = postList(tagVo.getMetadata().getName(), pageNum, path); + var postList = postList(tagVo.getMetadata().getName(), pageNum, path) + .doOnNext(list -> list.forEach(postVo -> + postVo.getSpec().setTitle( + titleVisibilityIdentifyCalculator.calculateTitle( + postVo.getSpec().getTitle(), + postVo.getSpec().getVisible(), + localeContextResolver.resolveLocaleContext(request.exchange()) + .getLocale() + ) + ) + )); return ServerResponse.ok() .render(DefaultTemplateEnum.TAG.getValue(), Map.of("name", tagVo.getMetadata().getName(), diff --git a/application/src/main/resources/config/i18n/messages.properties b/application/src/main/resources/config/i18n/messages.properties index 542077fb1..4870b7ffd 100644 --- a/application/src/main/resources/config/i18n/messages.properties +++ b/application/src/main/resources/config/i18n/messages.properties @@ -55,3 +55,5 @@ problemDetail.plugin.version.unsatisfied.requires=Plugin requires a minimum syst problemDetail.plugin.missingManifest=Missing plugin manifest file "plugin.yaml" or manifest file does not conform to the specification. problemDetail.internalServerError=Something went wrong, please try again later. problemDetail.migration.backup.notFound=The backup file does not exist or has been deleted. + +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 3dff2acca..cfe6ef98d 100644 --- a/application/src/main/resources/config/i18n/messages_zh.properties +++ b/application/src/main/resources/config/i18n/messages_zh.properties @@ -26,3 +26,5 @@ problemDetail.theme.version.unsatisfied.requires=主题要求一个最小的系 problemDetail.theme.install.missingManifest=缺少 theme.yaml 配置文件或配置文件不符合规范。 problemDetail.internalServerError=服务器内部发生错误,请稍候再试。 problemDetail.migration.backup.notFound=备份文件不存在或已删除。 + +title.visibility.identification.private=(私有) \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java b/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java index 6c783cf0d..a9b5d01ed 100644 --- a/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java +++ b/application/src/test/java/run/halo/app/theme/finders/impl/PostFinderImplTest.java @@ -4,13 +4,13 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; -import static run.halo.app.theme.finders.PostPublicQueryService.FIXED_PREDICATE; import java.time.Instant; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Predicate; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.util.Strings; import org.junit.jupiter.api.Test; @@ -32,6 +32,7 @@ import run.halo.app.theme.finders.TagFinder; import run.halo.app.theme.finders.vo.ListedPostVo; import run.halo.app.theme.finders.vo.PostArchiveVo; import run.halo.app.theme.finders.vo.PostArchiveYearMonthVo; +import run.halo.app.theme.router.DefaultQueryPostPredicateResolver; /** * Tests for {@link PostFinderImpl}. @@ -77,7 +78,10 @@ class PostFinderImplTest { @Test void predicate() { - List strings = posts().stream().filter(FIXED_PREDICATE) + Predicate predicate = new DefaultQueryPostPredicateResolver().getPredicate().block(); + assertThat(predicate).isNotNull(); + + List strings = posts().stream().filter(predicate) .map(post -> post.getMetadata().getName()) .toList(); assertThat(strings).isEqualTo(List.of("post-1", "post-2", "post-6")); diff --git a/application/src/test/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolverTest.java b/application/src/test/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolverTest.java new file mode 100644 index 000000000..6e675f7e0 --- /dev/null +++ b/application/src/test/java/run/halo/app/theme/router/ReactiveQueryPostPredicateResolverTest.java @@ -0,0 +1,87 @@ +package run.halo.app.theme.router; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import reactor.test.StepVerifier; +import run.halo.app.core.extension.content.Post; +import run.halo.app.extension.Metadata; + +/** + * Tests for {@link ReactiveQueryPostPredicateResolver}. + * + * @author guqing + * @since 2.9.0 + */ +@ExtendWith(SpringExtension.class) +class ReactiveQueryPostPredicateResolverTest { + + private ReactiveQueryPostPredicateResolver postPredicateResolver; + + @BeforeEach + void setUp() { + postPredicateResolver = new DefaultQueryPostPredicateResolver(); + } + + @Test + void getPredicateWithoutAuth() { + postPredicateResolver.getPredicate() + .as(StepVerifier::create) + .consumeNextWith(predicate -> { + Post post = new Post(); + post.setMetadata(new Metadata()); + post.getMetadata().setName("fake-post"); + + post.setSpec(new Post.PostSpec()); + post.getSpec().setDeleted(false); + post.getMetadata().setLabels(Map.of(Post.PUBLISHED_LABEL, "true")); + post.getSpec().setVisible(Post.VisibleEnum.PRIVATE); + assertThat(predicate.test(post)).isFalse(); + + post.getSpec().setVisible(Post.VisibleEnum.PUBLIC); + assertThat(predicate.test(post)).isTrue(); + + post.getMetadata().setLabels(Map.of(Post.PUBLISHED_LABEL, "false")); + assertThat(predicate.test(post)).isFalse(); + }) + .verifyComplete(); + } + + @Test + @WithMockUser(username = "halo") + void getPredicateWithAuth() { + postPredicateResolver.getPredicate() + .as(StepVerifier::create) + .consumeNextWith(predicate -> { + Post post = new Post(); + post.setMetadata(new Metadata()); + post.getMetadata().setName("fake-post"); + + post.setSpec(new Post.PostSpec()); + post.getSpec().setDeleted(false); + post.getSpec().setOwner("halo"); + post.getMetadata().setLabels(Map.of(Post.PUBLISHED_LABEL, "true")); + post.getSpec().setVisible(Post.VisibleEnum.PRIVATE); + assertThat(predicate.test(post)).isTrue(); + + post.getSpec().setOwner("guqing"); + assertThat(predicate.test(post)).isFalse(); + + post.getSpec().setOwner("halo"); + post.getSpec().setVisible(Post.VisibleEnum.PUBLIC); + assertThat(predicate.test(post)).isTrue(); + + post.getSpec().setDeleted(true); + assertThat(predicate.test(post)).isFalse(); + + post.getSpec().setVisible(Post.VisibleEnum.INTERNAL); + assertThat(predicate.test(post)).isFalse(); + }) + .verifyComplete(); + } +} \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/theme/router/SinglePageRouteTest.java b/application/src/test/java/run/halo/app/theme/router/SinglePageRouteTest.java index 657482b3d..249978c99 100644 --- a/application/src/test/java/run/halo/app/theme/router/SinglePageRouteTest.java +++ b/application/src/test/java/run/halo/app/theme/router/SinglePageRouteTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.when; import java.net.URI; import java.time.Instant; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Optional; import org.junit.jupiter.api.Nested; @@ -21,6 +22,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.i18n.SimpleLocaleContext; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; @@ -35,6 +37,7 @@ import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.i18n.LocaleContextResolver; import org.springframework.web.util.UriUtils; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -69,6 +72,12 @@ class SinglePageRouteTest { @Mock ExtensionClient client; + @Mock + LocaleContextResolver localeContextResolver; + + @Mock + TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + @InjectMocks SinglePageRoute singlePageRoute; @@ -115,6 +124,8 @@ class SinglePageRouteTest { .build()) .build(); + when(localeContextResolver.resolveLocaleContext(any())) + .thenReturn(new SimpleLocaleContext(Locale.getDefault())); webTestClient.get() .uri("/archives/fake-name") .exchange() diff --git a/application/src/test/java/run/halo/app/theme/router/factories/PostRouteFactoryTest.java b/application/src/test/java/run/halo/app/theme/router/factories/PostRouteFactoryTest.java index 9319d463b..01008cab8 100644 --- a/application/src/test/java/run/halo/app/theme/router/factories/PostRouteFactoryTest.java +++ b/application/src/test/java/run/halo/app/theme/router/factories/PostRouteFactoryTest.java @@ -5,17 +5,20 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import java.util.Locale; import java.util.Map; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.i18n.SimpleLocaleContext; import org.springframework.http.MediaType; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.i18n.LocaleContextResolver; import reactor.core.publisher.Mono; import run.halo.app.content.TestPost; import run.halo.app.core.extension.content.Post; @@ -25,8 +28,11 @@ import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.vo.PostVo; +import run.halo.app.theme.router.DefaultQueryPostPredicateResolver; import run.halo.app.theme.router.EmptyView; import run.halo.app.theme.router.ModelConst; +import run.halo.app.theme.router.ReactiveQueryPostPredicateResolver; +import run.halo.app.theme.router.TitleVisibilityIdentifyCalculator; import run.halo.app.theme.router.ViewNameResolver; /** @@ -47,6 +53,15 @@ class PostRouteFactoryTest extends RouteFactoryTestSuite { @Mock private ReactiveExtensionClient client; + @Mock + private ReactiveQueryPostPredicateResolver predicateResolver; + + @Mock + private LocaleContextResolver localeContextResolver; + + @Mock + private TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator; + @InjectMocks private PostRouteFactory postRouteFactory; @@ -64,10 +79,14 @@ class PostRouteFactoryTest extends RouteFactoryTestSuite { when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any())) .thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue())); + when(predicateResolver.getPredicate()) + .thenReturn(new DefaultQueryPostPredicateResolver().getPredicate()); RouterFunction routerFunction = postRouteFactory.create("/archives/{name}"); WebTestClient webTestClient = getWebTestClient(routerFunction); + when(localeContextResolver.resolveLocaleContext(any())) + .thenReturn(new SimpleLocaleContext(Locale.getDefault())); when(viewResolver.resolveViewName(any(), any())) .thenReturn(Mono.just(new EmptyView() { @Override