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 d0f608219..fdcd2e72d 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 @@ -1,24 +1,29 @@ package run.halo.app.theme.router; -import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static org.springframework.web.reactive.function.server.RequestPredicates.methods; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.HandlerFunction; +import org.springframework.web.reactive.function.server.RequestPredicate; import org.springframework.web.reactive.function.server.RequestPredicates; 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.util.UriUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import run.halo.app.core.extension.content.SinglePage; @@ -41,7 +46,7 @@ import run.halo.app.theme.finders.SinglePageFinder; @RequiredArgsConstructor public class SinglePageRoute implements RouterFunction, Reconciler, DisposableBean { - private final Map> quickRouteMap = + private Map> quickRouteMap = new ConcurrentHashMap<>(); private final ExtensionClient client; @@ -58,6 +63,15 @@ public class SinglePageRoute .next(); } + /** + * Set quickRouteMap. This method is only for testing. + * + * @param quickRouteMap fresh quickRouteMap. + */ + void setQuickRouteMap(Map> quickRouteMap) { + this.quickRouteMap = quickRouteMap; + } + @Override public void accept(@NonNull RouterFunctions.Visitor visitor) { routerFunctions().forEach(routerFunction -> routerFunction.accept(visitor)); @@ -66,14 +80,23 @@ public class SinglePageRoute private List> routerFunctions() { return quickRouteMap.keySet().stream() .map(nameSlugPair -> { - String routePath = singlePageRoute(nameSlugPair.slug()); - return RouterFunctions.route(GET(routePath) + var routePath = singlePageRoute(nameSlugPair.slug()); + return RouterFunctions.route(methods(HttpMethod.GET) + .and(exactPath(routePath)) .and(RequestPredicates.accept(MediaType.TEXT_HTML)), handlerFunction(nameSlugPair.name())); }) .collect(Collectors.toList()); } + private RequestPredicate exactPath(String path) { + return request -> { + var encodedRoutePath = UriUtils.encodePath(path, StandardCharsets.UTF_8); + var requestPath = request.requestPath().pathWithinApplication().value(); + return Objects.equals(requestPath, encodedRoutePath); + }; + } + @Override public Result reconcile(Request request) { client.fetch(SinglePage.class, request.name()) 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 f627d1245..d12ab8f54 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 @@ -1,17 +1,24 @@ package run.halo.app.theme.router; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.when; +import java.net.URI; 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.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.reactive.function.server.MockServerRequest; +import org.springframework.mock.web.server.MockServerWebExchange; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.HandlerStrategies; @@ -20,13 +27,16 @@ 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.util.UriUtils; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import run.halo.app.core.extension.content.SinglePage; import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.Metadata; import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.vo.SinglePageVo; +import run.halo.app.theme.router.SinglePageRoute.NameSlugPair; /** * Tests for {@link SinglePageRoute}. @@ -97,4 +107,27 @@ class SinglePageRouteTest { .exchange() .expectStatus().isOk(); } + + @Test + void shouldNotThrowErrorIfSlugNameContainsSpecialChars() { + var specialChars = "/with-special-chars-{}-[]-{{}}-{[]}-[{}]"; + var specialCharsUri = + URI.create(UriUtils.encodePath(specialChars, UTF_8)); + var mockHttpRequest = MockServerHttpRequest.get(specialCharsUri.toString()) + .accept(MediaType.TEXT_HTML) + .build(); + var mockExchange = MockServerWebExchange.from(mockHttpRequest); + var request = MockServerRequest.builder() + .exchange(mockExchange) + .uri(specialCharsUri) + .method(HttpMethod.GET) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_HTML_VALUE) + .build(); + var nameSlugPair = new NameSlugPair("fake-single-page", specialChars); + singlePageRoute.setQuickRouteMap(Map.of(nameSlugPair, r -> ServerResponse.ok().build())); + StepVerifier.create(singlePageRoute.route(request)) + .expectNextCount(1) + .verifyComplete(); + } + }