mirror of https://github.com/halo-dev/halo
refactor: ThemeLinkBuilder needs to rely on external url (#2661)
parent
84b28cec16
commit
36613c0442
|
@ -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());
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue