Fix the problem of not being able to request SinglePage with slug name containing special characters (#2479)

#### What type of PR is this?

/kind bug
/kind api-change
/area core
/milestone 2.0

#### What this PR does / why we need it:

Support setting special characters on slug name of SinglePage.

#### Which issue(s) this PR fixes:

Partial Fixes https://github.com/halo-dev/halo/issues/2473

#### Special notes for your reviewer:

Steps to test:

1. Create single pags with slug name `中文`, `/a/b/c/d` or `a / b`
2. Request it with permalink generated by Halo

#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/2493/head
John Niang 2022-09-28 15:48:21 +08:00 committed by GitHub
parent e954af88ec
commit 565c8340e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 35 deletions

View File

@ -1,5 +1,8 @@
package run.halo.app.core.extension.reconciler; package run.halo.app.core.extension.reconciler;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.web.util.UriUtils.encodePath;
import java.time.Instant; import java.time.Instant;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -150,8 +153,10 @@ public class SinglePageReconciler implements Reconciler<Reconciler.Request> {
final SinglePage oldPage = JsonUtils.deepCopy(singlePage); final SinglePage oldPage = JsonUtils.deepCopy(singlePage);
permalinkOnDelete(oldPage); permalinkOnDelete(oldPage);
var permalink = encodePath(singlePage.getSpec().getSlug(), UTF_8);
permalink = StringUtils.prependIfMissing(permalink, "/");
singlePage.getStatusOrDefault() singlePage.getStatusOrDefault()
.setPermalink(PathUtils.combinePath(singlePage.getSpec().getSlug())); .setPermalink(permalink);
if (isPublished(singlePage)) { if (isPublished(singlePage)) {
permalinkOnAdd(singlePage); permalinkOnAdd(singlePage);
} }

View File

@ -200,7 +200,6 @@ public class SpringWebFluxTemplateEngine extends SpringTemplateEngine
try { try {
process(templateName, markupSelectors, context, writer); process(templateName, markupSelectors, context, writer);
Mono.empty().block();
} catch (final Throwable t) { } catch (final Throwable t) {
logger.error( logger.error(

View File

@ -157,6 +157,24 @@ public class PermalinkIndexer {
} }
} }
/**
* Get extension name by permalink.
*
* @param gvk is GroupVersionKind of extension
* @param permalink is encoded permalink
* @return extension name or null
*/
@Nullable
public String getNameByPermalink(GroupVersionKind gvk, String permalink) {
readWriteLock.readLock().lock();
try {
var locator = permalinkLocatorLookup.get(permalink);
return locator == null ? null : locator.name();
} finally {
readWriteLock.readLock().unlock();
}
}
/** /**
* Only for test. * Only for test.
* *

View File

@ -0,0 +1,17 @@
package run.halo.app.theme.router.strategy;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.springframework.web.reactive.function.server.RequestPredicates.method;
import static org.springframework.web.reactive.function.server.RequestPredicates.path;
import org.springframework.http.HttpMethod;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.util.UriUtils;
public enum PermalinkPredicates {
;
public static RequestPredicate get(String permalink) {
return method(HttpMethod.GET).and(path(UriUtils.decode(permalink, UTF_8)));
}
}

View File

@ -1,14 +1,13 @@
package run.halo.app.theme.router.strategy; package run.halo.app.theme.router.strategy;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept; import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static run.halo.app.theme.router.strategy.PermalinkPredicates.get;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RequestPredicate; 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.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions; import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
@ -49,13 +48,12 @@ public class SinglePageRouteStrategy implements TemplateRouterStrategy {
List<String> permalinks = permalinkIndexer.getPermalinks(gvk); List<String> permalinks = permalinkIndexer.getPermalinks(gvk);
for (String permalink : permalinks) { for (String permalink : permalinks) {
requestPredicate = requestPredicate.or(RequestPredicates.GET(permalink)); requestPredicate = requestPredicate.or(get(permalink));
} }
return RouterFunctions return RouterFunctions
.route(requestPredicate.and(accept(MediaType.TEXT_HTML)), request -> { .route(requestPredicate.and(accept(MediaType.TEXT_HTML)), request -> {
String slug = StringUtils.removeStart(request.path(), "/"); var name = permalinkIndexer.getNameByPermalink(gvk, request.path());
String name = permalinkIndexer.getNameBySlug(gvk, slug);
if (name == null) { if (name == null) {
return ServerResponse.notFound().build(); return ServerResponse.notFound().build();
} }

View File

@ -2,6 +2,8 @@ package run.halo.app.theme.router;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
@ -106,4 +108,16 @@ class PermalinkIndexerTest {
permalinkIndexer.getNameBySlug(gvk, "nothing"); permalinkIndexer.getNameBySlug(gvk, "nothing");
}).isInstanceOf(NoSuchElementException.class); }).isInstanceOf(NoSuchElementException.class);
} }
@Test
void getNameByPermalink() {
ExtensionLocator locator = new ExtensionLocator(gvk, "test-name", "test-slug");
permalinkIndexer.register(locator, "/test-permalink");
var name = permalinkIndexer.getNameByPermalink(gvk, "/test-permalink");
assertEquals("test-name", name);
name = permalinkIndexer.getNameByPermalink(gvk, "/invalid-permalink");
assertNull(name);
}
} }

View File

@ -4,7 +4,9 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static run.halo.app.theme.DefaultTemplateEnum.SINGLE_PAGE;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@ -13,15 +15,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.HandlerStrategies; import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.reactive.result.view.ViewResolver;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.SinglePageFinder;
import run.halo.app.theme.router.PermalinkIndexer; import run.halo.app.theme.router.PermalinkIndexer;
@ -50,37 +48,66 @@ class SinglePageRouteStrategyTest {
void setUp() { void setUp() {
lenient().when(singlePageFinder.list(anyInt(), anyInt())) lenient().when(singlePageFinder.list(anyInt(), anyInt()))
.thenReturn(new ListResult<>(1, 10, 0, List.of())); .thenReturn(new ListResult<>(1, 10, 0, List.of()));
when(permalinkIndexer.getPermalinks(any())) lenient().when(viewResolver.resolveViewName(eq(SINGLE_PAGE.getValue()), any()))
.thenReturn(List.of("/fake-slug")); .thenReturn(Mono.just(new EmptyView()));
when(permalinkIndexer.getNameBySlug(any(), eq("fake-slug")))
.thenReturn("fake-name");
} }
@Test @Test
void getRouteFunction() { void shouldResponse404IfNoPermalinkFound() {
RouterFunction<ServerResponse> routeFunction = createClient().get()
strategy.getRouteFunction(DefaultTemplateEnum.SINGLE_PAGE.getValue(), .uri("/nothing")
null); .exchange()
.expectStatus().isNotFound();
}
WebTestClient client = WebTestClient.bindToRouterFunction(routeFunction) @Test
.handlerStrategies(HandlerStrategies.builder() void shouldResponse200IfPermalinkFound() {
.viewResolver(viewResolver) when(permalinkIndexer.getPermalinks(any()))
.build()) .thenReturn(List.of("/fake-slug"));
.build(); when(permalinkIndexer.getNameByPermalink(any(), eq("/fake-slug")))
.thenReturn("fake-name");
when(viewResolver.resolveViewName(eq(DefaultTemplateEnum.SINGLE_PAGE.getValue()), any())) createClient().get()
.thenReturn(Mono.just(new EmptyView()));
client.get()
.uri("/fake-slug") .uri("/fake-slug")
.exchange() .exchange()
.expectStatus() .expectStatus()
.isOk(); .isOk();
client.get() verify(permalinkIndexer).getNameByPermalink(any(), eq("/fake-slug"));
.uri("/nothing")
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.NOT_FOUND);
} }
}
@Test
void shouldResponse200IfSlugNameContainsSpecialChars() {
when(permalinkIndexer.getPermalinks(any()))
.thenReturn(List.of("/fake%20/%20slug"));
when(permalinkIndexer.getNameByPermalink(any(), eq("/fake%20/%20slug")))
.thenReturn("fake-name");
createClient().get()
.uri("/fake / slug")
.exchange()
.expectStatus().isOk();
verify(permalinkIndexer).getNameByPermalink(any(), eq("/fake%20/%20slug"));
}
@Test
void shouldResponse200IfSlugNameContainsChineseChars() {
when(permalinkIndexer.getPermalinks(any()))
.thenReturn(List.of("/%E4%B8%AD%E6%96%87"));
when(permalinkIndexer.getNameByPermalink(any(), eq("/%E4%B8%AD%E6%96%87")))
.thenReturn("fake-name");
createClient().get()
.uri("/中文")
.exchange()
.expectStatus().isOk();
verify(permalinkIndexer).getNameByPermalink(any(), eq("/%E4%B8%AD%E6%96%87"));
}
WebTestClient createClient() {
var routeFunction = strategy.getRouteFunction(SINGLE_PAGE.getValue(), null);
return WebTestClient.bindToRouterFunction(routeFunction)
.handlerStrategies(HandlerStrategies.builder()
.viewResolver(viewResolver)
.build())
.build();
}
}