feat: support displaying private posts for owner on theme-side (#4412)

#### 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
登录后支持在主题端展示作者的私有文章
```
pull/4482/head
guqing 2023-08-25 22:12:12 +08:00 committed by GitHub
parent 5e21909e36
commit 637071b260
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 396 additions and 44 deletions

View File

@ -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<Post> 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.

View File

@ -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<PostVo> getByName(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 -> postPublicQueryService.convertToVo(post,
post.getSpec().getReleaseSnapshot())
)
);
}
@ -65,7 +67,10 @@ public class PostFinderImpl implements PostFinder {
@Override
public Mono<NavigationPostVo> 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<ListedPostVo> listAll() {
return client.list(Post.class, FIXED_PREDICATE, defaultComparator())
return postPredicateResolver.getPredicate()
.flatMapMany(predicate -> client.list(Post.class, predicate, defaultComparator()))
.concatMap(postPublicQueryService::convertToListedVo);
}

View File

@ -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<ListResult<ListedPostVo>> list(Integer page, Integer size,
Predicate<Post> postPredicate, Comparator<Post> comparator) {
Predicate<Post> 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<PostVo> 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<ContentVo> 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())

View File

@ -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<SinglePageVo> 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<Predicate<SinglePage>> queryPredicate() {
Predicate<SinglePage> predicate = page -> page.isPublished()
&& Objects.equals(false, page.getSpec().getDeleted());
Predicate<SinglePage> 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<String> currentUserName() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Principal::getName)
.filter(name -> !AnonymousUserConst.isAnonymousUser(name));
}
static Comparator<SinglePage> defaultComparator() {
Function<SinglePage, Boolean> pinned =
page -> Objects.requireNonNullElse(page.getSpec().getPinned(), false);

View File

@ -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<Predicate<Post>> getPredicate() {
Predicate<Post> predicate = post -> post.isPublished()
&& !ExtensionUtil.isDeleted(post)
&& Objects.equals(false, post.getSpec().getDeleted());
Predicate<Post> 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<String> currentUserName() {
return ReactiveSecurityContextHolder.getContext()
.map(SecurityContext::getAuthentication)
.map(Principal::getName)
.filter(name -> !AnonymousUserConst.isAnonymousUser(name));
}
}

View File

@ -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<Predicate<Post>> getPredicate();
}

View File

@ -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<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
@ -144,6 +149,14 @@ public class SinglePageRoute
HandlerFunction<ServerResponse> 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<String, Object> model = ModelMapUtils.singlePageModel(singlePageVo);
String template = singlePageVo.getSpec().getTemplate();

View File

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

View File

@ -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<ServerResponse> 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<PostArchiveVo>()
.listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(requestPath, totalPage(list)))

View File

@ -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<ServerResponse> 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<ListedPostVo>()
.listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))

View File

@ -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<ServerResponse> 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<ListedPostVo>()
.listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))

View File

@ -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<ServerResponse> create(String pattern) {
@ -54,8 +58,19 @@ public class IndexRouteFactory implements RouteFactory {
private Mono<UrlContextListResult<ListedPostVo>> 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<ListedPostVo>()
.listResult(list)
.nextUrl(PageUrlUtils.nextPageUrl(path, totalPage(list)))

View File

@ -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<ServerResponse> create(String pattern) {
PatternParser postParamPredicate =
@ -73,7 +82,7 @@ public class PostRouteFactory implements RouteFactory {
return request -> {
Map<String, String> 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<PostVo> 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<String, Object> model = ModelMapUtils.postModel(postVo);
String template = postVo.getSpec().getTemplate();
@ -133,16 +151,19 @@ public class PostRouteFactory implements RouteFactory {
}
private Flux<Post> 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<Post> 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<String, Matcher> 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;
}
}
}

View File

@ -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<ServerResponse> 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(),

View File

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

View File

@ -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=(私有)

View File

@ -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<String> strings = posts().stream().filter(FIXED_PREDICATE)
Predicate<Post> predicate = new DefaultQueryPostPredicateResolver().getPredicate().block();
assertThat(predicate).isNotNull();
List<String> strings = posts().stream().filter(predicate)
.map(post -> post.getMetadata().getName())
.toList();
assertThat(strings).isEqualTo(List.of("post-1", "post-2", "post-6"));

View File

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

View File

@ -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()

View File

@ -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<ServerResponse> 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