diff --git a/src/main/java/run/halo/app/theme/TemplateEngineManager.java b/src/main/java/run/halo/app/theme/TemplateEngineManager.java index d4f6529cf..1dac1662e 100644 --- a/src/main/java/run/halo/app/theme/TemplateEngineManager.java +++ b/src/main/java/run/halo/app/theme/TemplateEngineManager.java @@ -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 templateResolvers; private final ObjectProvider dialects; public TemplateEngineManager(ThymeleafProperties thymeleafProperties, + ExternalUrlSupplier externalUrlSupplier, ObjectProvider templateResolvers, ObjectProvider 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()); diff --git a/src/main/java/run/halo/app/theme/ThemeLinkBuilder.java b/src/main/java/run/halo/app/theme/ThemeLinkBuilder.java index cf90abec2..29407dc6c 100644 --- a/src/main/java/run/halo/app/theme/ThemeLinkBuilder.java +++ b/src/main/java/run/halo/app/theme/ThemeLinkBuilder.java @@ -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); } } diff --git a/src/test/java/run/halo/app/theme/ThemeLinkBuilderTest.java b/src/test/java/run/halo/app/theme/ThemeLinkBuilderTest.java index 5401f1a66..0b0a375cb 100644 --- a/src/test/java/run/halo/app/theme/ThemeLinkBuilderTest.java +++ b/src/test/java/run/halo/app/theme/ThemeLinkBuilderTest.java @@ -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) {