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.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());
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
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) == '/') {
|
||||
static boolean linkInSite(@NonNull URI externalUri, @NonNull String link) {
|
||||
if (!PathUtils.isAbsoluteUri(link)) {
|
||||
// relative uri is always in site
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue