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

View File

@ -1,9 +1,13 @@
package run.halo.app.theme; package run.halo.app.theme;
import java.net.URI;
import java.net.URISyntaxException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.thymeleaf.context.IExpressionContext; import org.thymeleaf.context.IExpressionContext;
import org.thymeleaf.linkbuilder.StandardLinkBuilder; import org.thymeleaf.linkbuilder.StandardLinkBuilder;
import run.halo.app.infra.ExternalUrlSupplier;
import run.halo.app.infra.utils.PathUtils; import run.halo.app.infra.utils.PathUtils;
/** /**
@ -15,14 +19,16 @@ public class ThemeLinkBuilder extends StandardLinkBuilder {
public static final String THEME_PREVIEW_PREFIX = "/themes"; public static final String THEME_PREVIEW_PREFIX = "/themes";
private final ThemeContext theme; private final ThemeContext theme;
private final ExternalUrlSupplier externalUrlSupplier;
public ThemeLinkBuilder(ThemeContext theme) { public ThemeLinkBuilder(ThemeContext theme, ExternalUrlSupplier externalUrlSupplier) {
this.theme = theme; this.theme = theme;
this.externalUrlSupplier = externalUrlSupplier;
} }
@Override @Override
protected String processLink(IExpressionContext context, String link) { protected String processLink(IExpressionContext context, String link) {
if (link == null || isLinkBaseAbsolute(link)) { if (link == null || !linkInSite(externalUrlSupplier.get(), link)) {
return link; return link;
} }
@ -44,38 +50,22 @@ public class ThemeLinkBuilder extends StandardLinkBuilder {
.build().toString(); .build().toString();
} }
private static boolean isLinkBaseAbsolute(final CharSequence linkBase) { static boolean linkInSite(@NonNull URI externalUri, @NonNull String link) {
final int linkBaseLen = linkBase.length(); if (!PathUtils.isAbsoluteUri(link)) {
if (linkBaseLen < 2) { // relative uri is always in site
return false;
}
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; return true;
} }
try {
URI requestUri = new URI(link);
return StringUtils.equals(externalUri.getAuthority(), requestUri.getAuthority());
} catch (URISyntaxException e) {
// ignore this link
} }
return false; return false;
} }
private boolean isAssetsRequest(String link) { 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; package run.halo.app.theme;
import static org.assertj.core.api.Assertions.assertThat; 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 java.nio.file.Paths;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; 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}. * Tests for {@link ThemeLinkBuilder}.
@ -11,12 +19,21 @@ import org.junit.jupiter.api.Test;
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
@ExtendWith(MockitoExtension.class)
class ThemeLinkBuilderTest { class ThemeLinkBuilderTest {
private ThemeLinkBuilder themeLinkBuilder; @Mock
private ExternalUrlSupplier externalUrlSupplier;
@BeforeEach
void setUp() {
// Mock external url supplier
lenient().when(externalUrlSupplier.get()).thenReturn(URI.create(""));
}
@Test @Test
void processTemplateLinkWithNoActive() { void processTemplateLinkWithNoActive() {
themeLinkBuilder = new ThemeLinkBuilder(getTheme(false)); ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);
String link = "/post"; String link = "/post";
String processed = themeLinkBuilder.processLink(null, link); String processed = themeLinkBuilder.processLink(null, link);
@ -28,7 +45,8 @@ class ThemeLinkBuilderTest {
@Test @Test
void processTemplateLinkWithActive() { void processTemplateLinkWithActive() {
themeLinkBuilder = new ThemeLinkBuilder(getTheme(true)); ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(true), externalUrlSupplier);
String link = "/post"; String link = "/post";
String processed = themeLinkBuilder.processLink(null, link); String processed = themeLinkBuilder.processLink(null, link);
@ -38,7 +56,8 @@ class ThemeLinkBuilderTest {
@Test @Test
void processAssetsLink() { void processAssetsLink() {
// activated theme // activated theme
themeLinkBuilder = new ThemeLinkBuilder(getTheme(true)); ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(true), externalUrlSupplier);
String link = "/assets/css/style.css"; String link = "/assets/css/style.css";
String processed = themeLinkBuilder.processLink(null, link); String processed = themeLinkBuilder.processLink(null, link);
@ -53,7 +72,8 @@ class ThemeLinkBuilderTest {
@Test @Test
void processNullLink() { void processNullLink() {
themeLinkBuilder = new ThemeLinkBuilder(getTheme(false)); ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);
String link = null; String link = null;
String processed = themeLinkBuilder.processLink(null, link); String processed = themeLinkBuilder.processLink(null, link);
@ -67,7 +87,8 @@ class ThemeLinkBuilderTest {
@Test @Test
void processAbsoluteLink() { void processAbsoluteLink() {
themeLinkBuilder = new ThemeLinkBuilder(getTheme(false)); ThemeLinkBuilder themeLinkBuilder =
new ThemeLinkBuilder(getTheme(false), externalUrlSupplier);
String link = "https://github.com/halo-dev"; String link = "https://github.com/halo-dev";
String processed = themeLinkBuilder.processLink(null, link); String processed = themeLinkBuilder.processLink(null, link);
assertThat(processed).isEqualTo(link); assertThat(processed).isEqualTo(link);
@ -75,10 +96,31 @@ class ThemeLinkBuilderTest {
link = "http://example.com"; link = "http://example.com";
processed = themeLinkBuilder.processLink(null, link); processed = themeLinkBuilder.processLink(null, link);
assertThat(processed).isEqualTo(link); assertThat(processed).isEqualTo(link);
}
link = "//example.com"; @Test
processed = themeLinkBuilder.processLink(null, link); void linkInSite() throws URISyntaxException {
assertThat(processed).isEqualTo(link); 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) { private ThemeContext getTheme(boolean isActive) {