refactor: ThemeLinkBuilder needs to rely on external url (#2661)

pull/2659/head
guqing 2022-11-08 09:33:38 +08:00 committed by GitHub
parent 84b28cec16
commit 36613c0442
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 39 deletions

View File

@ -11,6 +11,7 @@ import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
import org.thymeleaf.templateresolver.FileTemplateResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.exception.NotFoundException;
import run.halo.app.theme.dialect.HaloProcessorDialect;
import run.halo.app.theme.engine.SpringWebFluxTemplateEngine;
@ -38,14 +39,18 @@ public class TemplateEngineManager {
private final ThymeleafProperties thymeleafProperties;
private final ExternalUrlSupplier externalUrlSupplier;
private final ObjectProvider<ITemplateResolver> templateResolvers;
private final ObjectProvider<IDialect> dialects;
public TemplateEngineManager(ThymeleafProperties thymeleafProperties,
ExternalUrlSupplier externalUrlSupplier,
ObjectProvider<ITemplateResolver> templateResolvers,
ObjectProvider<IDialect> dialects) {
this.thymeleafProperties = thymeleafProperties;
this.externalUrlSupplier = externalUrlSupplier;
this.templateResolvers = templateResolvers;
this.dialects = dialects;
engineCache = new ConcurrentLruCache<>(CACHE_SIZE_LIMIT, this::templateEngineGenerator);
@ -74,7 +79,7 @@ public class TemplateEngineManager {
var engine = new SpringWebFluxTemplateEngine();
engine.setEnableSpringELCompiler(thymeleafProperties.isEnableSpringElCompiler());
engine.setMessageResolver(new ThemeMessageResolver(theme));
engine.setLinkBuilder(new ThemeLinkBuilder(theme));
engine.setLinkBuilder(new ThemeLinkBuilder(theme, externalUrlSupplier));
engine.setRenderHiddenMarkersBeforeCheckboxes(
thymeleafProperties.isRenderHiddenMarkersBeforeCheckboxes());

View File

@ -1,9 +1,13 @@
package run.halo.app.theme;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.web.util.UriComponentsBuilder;
import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.linkbuilder.StandardLinkBuilder;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.utils.PathUtils;
/**
@ -15,14 +19,16 @@ public class ThemeLinkBuilder extends StandardLinkBuilder {
public static final String THEME_PREVIEW_PREFIX = "/themes";
private final ThemeContext theme;
private final ExternalUrlSupplier externalUrlSupplier;
public ThemeLinkBuilder(ThemeContext theme) {
public ThemeLinkBuilder(ThemeContext theme, ExternalUrlSupplier externalUrlSupplier) {
this.theme = theme;
this.externalUrlSupplier = externalUrlSupplier;
}
@Override
protected String processLink(IExpressionContext context, String link) {
if (link == null || isLinkBaseAbsolute(link)) {
if (link == null || !linkInSite(externalUrlSupplier.get(), link)) {
return link;
}
@ -44,38 +50,22 @@ public class ThemeLinkBuilder extends StandardLinkBuilder {
.build().toString();
}
private static boolean isLinkBaseAbsolute(final CharSequence linkBase) {
final int linkBaseLen = linkBase.length();
if (linkBaseLen < 2) {
return false;
static boolean linkInSite(@NonNull URI externalUri, @NonNull String link) {
if (!PathUtils.isAbsoluteUri(link)) {
// relative uri is always in site
return true;
}
final char c0 = linkBase.charAt(0);
if (c0 == 'm' || c0 == 'M') {
// Let's check for "mailto:"
if (linkBase.length() >= 7
&& Character.toLowerCase(linkBase.charAt(1)) == 'a'
&& Character.toLowerCase(linkBase.charAt(2)) == 'i'
&& Character.toLowerCase(linkBase.charAt(3)) == 'l'
&& Character.toLowerCase(linkBase.charAt(4)) == 't'
&& Character.toLowerCase(linkBase.charAt(5)) == 'o'
&& Character.toLowerCase(linkBase.charAt(6)) == ':') {
return true;
}
} else if (c0 == '/') {
return linkBase.charAt(1)
== '/'; // It starts with '//' -> true, any other '/x' -> false
}
for (int i = 0; i < (linkBaseLen - 2); i++) {
// Let's try to find the '://' sequence anywhere in the base --> true
if (linkBase.charAt(i) == ':' && linkBase.charAt(i + 1) == '/'
&& linkBase.charAt(i + 2) == '/') {
return true;
}
try {
URI requestUri = new URI(link);
return StringUtils.equals(externalUri.getAuthority(), requestUri.getAuthority());
} catch (URISyntaxException e) {
// ignore this link
}
return false;
}
private boolean isAssetsRequest(String link) {
return link.startsWith(THEME_ASSETS_PREFIX);
String assetsPrefix = externalUrlSupplier.get().resolve(THEME_ASSETS_PREFIX).toString();
return link.startsWith(assetsPrefix) || link.startsWith(THEME_ASSETS_PREFIX);
}
}

View File

@ -1,9 +1,17 @@
package run.halo.app.theme;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Paths;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import run.halo.app.infra.ExternalUrlSupplier;
/**
* Tests for {@link ThemeLinkBuilder}.
@ -11,12 +19,21 @@ import org.junit.jupiter.api.Test;
* @author guqing
* @since 2.0.0
*/
@ExtendWith(MockitoExtension.class)
class ThemeLinkBuilderTest {
private ThemeLinkBuilder themeLinkBuilder;
@Mock
private ExternalUrlSupplier externalUrlSupplier;
@BeforeEach
void setUp() {
// Mock external url supplier
lenient().when(externalUrlSupplier.get()).thenReturn(URI.create(""));
}
@Test
void processTemplateLinkWithNoActive() {
themeLinkBuilder = new ThemeLinkBuilder(getTheme(false));
ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);
String link = "/post";
String processed = themeLinkBuilder.processLink(null, link);
@ -28,7 +45,8 @@ class ThemeLinkBuilderTest {
@Test
void processTemplateLinkWithActive() {
themeLinkBuilder = new ThemeLinkBuilder(getTheme(true));
ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(true), externalUrlSupplier);
String link = "/post";
String processed = themeLinkBuilder.processLink(null, link);
@ -38,7 +56,8 @@ class ThemeLinkBuilderTest {
@Test
void processAssetsLink() {
// activated theme
themeLinkBuilder = new ThemeLinkBuilder(getTheme(true));
ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(true), externalUrlSupplier);
String link = "/assets/css/style.css";
String processed = themeLinkBuilder.processLink(null, link);
@ -53,7 +72,8 @@ class ThemeLinkBuilderTest {
@Test
void processNullLink() {
themeLinkBuilder = new ThemeLinkBuilder(getTheme(false));
ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);
String link = null;
String processed = themeLinkBuilder.processLink(null, link);
@ -67,7 +87,8 @@ class ThemeLinkBuilderTest {
@Test
void processAbsoluteLink() {
themeLinkBuilder = new ThemeLinkBuilder(getTheme(false));
ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);
String link = "https://github.com/halo-dev";
String processed = themeLinkBuilder.processLink(null, link);
assertThat(processed).isEqualTo(link);
@ -75,10 +96,31 @@ class ThemeLinkBuilderTest {
link = "http://example.com";
processed = themeLinkBuilder.processLink(null, link);
assertThat(processed).isEqualTo(link);
}
link = "//example.com";
processed = themeLinkBuilder.processLink(null, link);
assertThat(processed).isEqualTo(link);
@Test
void linkInSite() throws URISyntaxException {
URI uri = new URI("");
// relative link is always in site
assertThat(ThemeLinkBuilder.linkInSite(uri, "/post")).isTrue();
// absolute link is not in site
assertThat(ThemeLinkBuilder.linkInSite(uri, "https://example.com")).isFalse();
uri = new URI("https://example.com");
// link in externalUrl is in site link
assertThat(ThemeLinkBuilder.linkInSite(uri, "http://example.com/hello/world")).isTrue();
// scheme is different but authority is same
assertThat(ThemeLinkBuilder.linkInSite(uri, "https://example.com/hello/world")).isTrue();
// scheme is same and authority is different
assertThat(ThemeLinkBuilder.linkInSite(uri, "http://halo.run/hello/world")).isFalse();
// scheme is different and authority is different
assertThat(ThemeLinkBuilder.linkInSite(uri, "https://halo.run/hello/world")).isFalse();
// port is different
uri = new URI("http://localhost:8090");
assertThat(ThemeLinkBuilder.linkInSite(uri, "http://localhost:3000")).isFalse();
}
private ThemeContext getTheme(boolean isActive) {