diff --git a/api/src/main/java/run/halo/app/infra/SystemSetting.java b/api/src/main/java/run/halo/app/infra/SystemSetting.java index 7df7fe465..e98561468 100644 --- a/api/src/main/java/run/halo/app/infra/SystemSetting.java +++ b/api/src/main/java/run/halo/app/infra/SystemSetting.java @@ -71,6 +71,7 @@ public class SystemSetting { String logo; String favicon; String language; + String externalUrl; @JsonIgnore public Optional useSystemLocale() { diff --git a/application/src/main/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplier.java b/application/src/main/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplier.java deleted file mode 100644 index d17c9e670..000000000 --- a/application/src/main/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplier.java +++ /dev/null @@ -1,71 +0,0 @@ -package run.halo.app.infra; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; -import org.springframework.http.HttpRequest; -import org.springframework.lang.Nullable; -import org.springframework.stereotype.Component; -import org.springframework.util.StringUtils; -import run.halo.app.infra.properties.HaloProperties; - -/** - * Default implementation for getting external url from halo properties. - * - * @author johnniang - */ -@Component -public class HaloPropertiesExternalUrlSupplier implements ExternalUrlSupplier { - - private final HaloProperties haloProperties; - - private final WebFluxProperties webFluxProperties; - - public HaloPropertiesExternalUrlSupplier(HaloProperties haloProperties, - WebFluxProperties webFluxProperties) { - this.haloProperties = haloProperties; - this.webFluxProperties = webFluxProperties; - } - - @Override - public URI get() { - if (!haloProperties.isUseAbsolutePermalink()) { - return URI.create(getBasePath()); - } - - try { - return haloProperties.getExternalUrl().toURI(); - } catch (URISyntaxException e) { - throw new RuntimeException(e); - } - } - - @Override - public URL getURL(HttpRequest request) { - var externalUrl = haloProperties.getExternalUrl(); - if (externalUrl == null) { - try { - externalUrl = request.getURI().resolve(getBasePath()).toURL(); - } catch (MalformedURLException e) { - throw new RuntimeException("Cannot parse request URI to URL.", e); - } - } - return externalUrl; - } - - @Nullable - @Override - public URL getRaw() { - return haloProperties.getExternalUrl(); - } - - private String getBasePath() { - var basePath = webFluxProperties.getBasePath(); - if (!StringUtils.hasText(basePath)) { - basePath = "/"; - } - return basePath; - } -} diff --git a/application/src/main/java/run/halo/app/infra/SystemConfigFirstExternalUrlSupplier.java b/application/src/main/java/run/halo/app/infra/SystemConfigFirstExternalUrlSupplier.java new file mode 100644 index 000000000..8f354dd93 --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/SystemConfigFirstExternalUrlSupplier.java @@ -0,0 +1,109 @@ +package run.halo.app.infra; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.time.Duration; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; +import org.springframework.context.event.EventListener; +import org.springframework.http.HttpRequest; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import reactor.core.Exceptions; +import run.halo.app.infra.properties.HaloProperties; + +/** + * Default implementation for getting external url from system config first, halo properties second. + * + * @author johnniang + */ +@Slf4j +@Component +class SystemConfigFirstExternalUrlSupplier implements ExternalUrlSupplier { + + private final HaloProperties haloProperties; + + private final WebFluxProperties webFluxProperties; + + private final SystemConfigurableEnvironmentFetcher systemConfigFetcher; + + @Nullable + private URL externalUrl; + + public SystemConfigFirstExternalUrlSupplier(HaloProperties haloProperties, + WebFluxProperties webFluxProperties, + SystemConfigurableEnvironmentFetcher systemConfigFetcher) { + this.haloProperties = haloProperties; + this.webFluxProperties = webFluxProperties; + this.systemConfigFetcher = systemConfigFetcher; + } + + @EventListener + void onExtensionInitialized(ExtensionInitializedEvent ignored) { + systemConfigFetcher.getBasic() + .mapNotNull(SystemSetting.Basic::getExternalUrl) + .filter(StringUtils::hasText) + .doOnNext(externalUrlString -> { + try { + this.externalUrl = URI.create(externalUrlString).toURL(); + } catch (MalformedURLException e) { + log.error(""" + Cannot parse external URL {} from system config. Fallback to default \ + external URL supplier from properties.\ + """, externalUrlString, e); + // For continuing the application startup, we need to return null here. + } + }) + .blockOptional(Duration.ofSeconds(10)); + } + + @Override + public URI get() { + try { + if (externalUrl != null) { + return externalUrl.toURI(); + } + if (!haloProperties.isUseAbsolutePermalink()) { + return URI.create(getBasePath()); + } + + return haloProperties.getExternalUrl().toURI(); + } catch (URISyntaxException e) { + throw Exceptions.propagate(e); + } + } + + @Override + public URL getURL(HttpRequest request) { + if (this.externalUrl != null) { + return this.externalUrl; + } + var externalUrl = haloProperties.getExternalUrl(); + if (externalUrl != null) { + return externalUrl; + } + try { + externalUrl = request.getURI().resolve(getBasePath()).toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException("Cannot parse request URI to URL.", e); + } + return externalUrl; + } + + @Nullable + @Override + public URL getRaw() { + return externalUrl != null ? externalUrl : haloProperties.getExternalUrl(); + } + + private String getBasePath() { + var basePath = webFluxProperties.getBasePath(); + if (!StringUtils.hasText(basePath)) { + basePath = "/"; + } + return basePath; + } +} diff --git a/application/src/main/java/run/halo/app/infra/SystemConfigurableEnvironmentFetcher.java b/application/src/main/java/run/halo/app/infra/SystemConfigurableEnvironmentFetcher.java index 1d59c19bd..2f75074c1 100644 --- a/application/src/main/java/run/halo/app/infra/SystemConfigurableEnvironmentFetcher.java +++ b/application/src/main/java/run/halo/app/infra/SystemConfigurableEnvironmentFetcher.java @@ -61,17 +61,17 @@ public class SystemConfigurableEnvironmentFetcher implements Reconciler getBasic() { return fetch(SystemSetting.Basic.GROUP, SystemSetting.Basic.class) - .switchIfEmpty(Mono.just(new SystemSetting.Basic())); + .switchIfEmpty(Mono.fromSupplier(SystemSetting.Basic::new)); } public Mono fetchComment() { return fetch(SystemSetting.Comment.GROUP, SystemSetting.Comment.class) - .switchIfEmpty(Mono.just(new SystemSetting.Comment())); + .switchIfEmpty(Mono.fromSupplier(SystemSetting.Comment::new)); } public Mono fetchPost() { return fetch(SystemSetting.Post.GROUP, SystemSetting.Post.class) - .switchIfEmpty(Mono.just(new SystemSetting.Post())); + .switchIfEmpty(Mono.fromSupplier(SystemSetting.Post::new)); } public Mono fetchRouteRules() { @@ -123,15 +123,17 @@ public class SystemConfigurableEnvironmentFetcher implements Reconciler { // https://www.rfc-editor.org/rfc/rfc7386 String defaultV = copiedDefault.get(group); - String newValue; + String newValue = null; if (dataValue == null) { - if (copiedDefault.containsKey(group)) { - newValue = null; - } else { + if (!copiedDefault.containsKey(group)) { newValue = defaultV; } } else { - newValue = mergeRemappingFunction(dataValue, defaultV); + if (copiedDefault.containsKey(group)) { + newValue = mergeRemappingFunction(dataValue, defaultV); + } else { + newValue = dataValue; + } } if (newValue == null) { @@ -195,20 +197,18 @@ public class SystemConfigurableEnvironmentFetcher implements Reconcilersystem by json merge patch. */ private Mono loadConfigMapInternal() { - Mono mapMono = + var defaultConfigMono = extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG_DEFAULT); - if (mapMono == null) { - return Mono.empty(); - } - return mapMono.flatMap(systemDefault -> - extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG) - .map(system -> { - Map defaultData = systemDefault.getData(); - Map data = system.getData(); + var configMono = extensionClient.fetch(ConfigMap.class, SystemSetting.SYSTEM_CONFIG); + return defaultConfigMono.flatMap(defaultConfig -> configMono.map( + config -> { + Map defaultData = defaultConfig.getData(); + Map data = config.getData(); Map mergedData = mergeData(defaultData, data); - system.setData(mergedData); - return system; + config.setData(mergedData); + return config; }) - .switchIfEmpty(Mono.just(systemDefault))); + .defaultIfEmpty(defaultConfig) + ); } } diff --git a/application/src/main/java/run/halo/app/infra/actuator/GlobalInfoServiceImpl.java b/application/src/main/java/run/halo/app/infra/actuator/GlobalInfoServiceImpl.java index 77059f978..ce6352ab3 100644 --- a/application/src/main/java/run/halo/app/infra/actuator/GlobalInfoServiceImpl.java +++ b/application/src/main/java/run/halo/app/infra/actuator/GlobalInfoServiceImpl.java @@ -12,6 +12,7 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import run.halo.app.extension.ConfigMap; +import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.InitializationStateGetter; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; @@ -36,20 +37,24 @@ public class GlobalInfoServiceImpl implements GlobalInfoService { private final ObjectProvider systemConfigFetcher; + private final ExternalUrlSupplier externalUrl; + public GlobalInfoServiceImpl(HaloProperties haloProperties, AuthProviderService authProviderService, InitializationStateGetter initializationStateGetter, - ObjectProvider systemConfigFetcher) { + ObjectProvider systemConfigFetcher, + ExternalUrlSupplier externalUrl) { this.haloProperties = haloProperties; this.authProviderService = authProviderService; this.initializationStateGetter = initializationStateGetter; this.systemConfigFetcher = systemConfigFetcher; + this.externalUrl = externalUrl; } @Override public Mono getGlobalInfo() { final var info = new GlobalInfo(); - info.setExternalUrl(haloProperties.getExternalUrl()); + info.setExternalUrl(externalUrl.getRaw()); info.setUseAbsolutePermalink(haloProperties.isUseAbsolutePermalink()); info.setLocale(Locale.getDefault()); info.setTimeZone(TimeZone.getDefault()); diff --git a/application/src/test/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplierTest.java b/application/src/test/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplierTest.java deleted file mode 100644 index 4fe085e3d..000000000 --- a/application/src/test/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplierTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package run.halo.app.infra; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; -import org.springframework.http.HttpRequest; -import run.halo.app.infra.properties.HaloProperties; - -@ExtendWith(MockitoExtension.class) -class HaloPropertiesExternalUrlSupplierTest { - - @Mock - HaloProperties haloProperties; - - @Mock - WebFluxProperties webFluxProperties; - - @InjectMocks - HaloPropertiesExternalUrlSupplier externalUrl; - - @Test - void getURIWhenUsingAbsolutePermalink() throws MalformedURLException { - var fakeUri = URI.create("https://halo.run/fake"); - when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); - when(haloProperties.isUseAbsolutePermalink()).thenReturn(true); - - assertEquals(fakeUri, externalUrl.get()); - } - - @Test - void getURIWhenBasePathSetAndNotUsingAbsolutePermalink() throws MalformedURLException { - when(webFluxProperties.getBasePath()).thenReturn("/blog"); - when(haloProperties.isUseAbsolutePermalink()).thenReturn(false); - - assertEquals(URI.create("/blog"), externalUrl.get()); - } - - @Test - void getURIWhenBasePathSetAndUsingAbsolutePermalink() throws MalformedURLException { - var fakeUri = URI.create("https://halo.run/fake"); - when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); - lenient().when(webFluxProperties.getBasePath()).thenReturn("/blog"); - when(haloProperties.isUseAbsolutePermalink()).thenReturn(true); - - assertEquals(URI.create("https://halo.run/fake"), externalUrl.get()); - } - - - @Test - void getURIWhenUsingRelativePermalink() throws MalformedURLException { - when(haloProperties.isUseAbsolutePermalink()).thenReturn(false); - - assertEquals(URI.create("/"), externalUrl.get()); - } - - @Test - void getURLWhenExternalURLProvided() throws MalformedURLException { - var fakeUri = URI.create("https://halo.run/fake"); - when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); - var mockRequest = mock(HttpRequest.class); - var url = externalUrl.getURL(mockRequest); - assertEquals(fakeUri.toURL(), url); - } - - @Test - void getURLWhenExternalURLAbsent() throws MalformedURLException { - var fakeUri = URI.create("https://localhost/fake"); - when(haloProperties.getExternalUrl()).thenReturn(null); - var mockRequest = mock(HttpRequest.class); - when(mockRequest.getURI()).thenReturn(fakeUri); - var url = externalUrl.getURL(mockRequest); - assertEquals(new URL("https://localhost/"), url); - } - - @Test - void getURLWhenBasePathSetAndExternalURLProvided() throws MalformedURLException { - var fakeUri = URI.create("https://localhost/fake"); - when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); - lenient().when(webFluxProperties.getBasePath()).thenReturn("/blog"); - var mockRequest = mock(HttpRequest.class); - lenient().when(mockRequest.getURI()).thenReturn(fakeUri); - var url = externalUrl.getURL(mockRequest); - assertEquals(new URL("https://localhost/fake"), url); - } - - @Test - void getURLWhenBasePathSetAndExternalURLAbsent() throws MalformedURLException { - var fakeUri = URI.create("https://localhost/fake"); - when(haloProperties.getExternalUrl()).thenReturn(null); - when(webFluxProperties.getBasePath()).thenReturn("/blog"); - var mockRequest = mock(HttpRequest.class); - when(mockRequest.getURI()).thenReturn(fakeUri); - var url = externalUrl.getURL(mockRequest); - assertEquals(new URL("https://localhost/blog"), url); - } - - @Test - void getRaw() throws MalformedURLException { - var fakeUri = URI.create("http://localhost/fake"); - when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); - assertEquals(fakeUri.toURL(), externalUrl.getRaw()); - - when(haloProperties.getExternalUrl()).thenReturn(null); - assertNull(externalUrl.getRaw()); - } - -} \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/infra/SystemConfigFirstExternalUrlSupplierTest.java b/application/src/test/java/run/halo/app/infra/SystemConfigFirstExternalUrlSupplierTest.java new file mode 100644 index 000000000..9c2c36671 --- /dev/null +++ b/application/src/test/java/run/halo/app/infra/SystemConfigFirstExternalUrlSupplierTest.java @@ -0,0 +1,155 @@ +package run.halo.app.infra; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxProperties; +import org.springframework.http.HttpRequest; +import reactor.core.publisher.Mono; +import run.halo.app.infra.properties.HaloProperties; + +@ExtendWith(MockitoExtension.class) +class SystemConfigFirstExternalUrlSupplierTest { + + @Mock + HaloProperties haloProperties; + + @Mock + WebFluxProperties webFluxProperties; + + @Mock + SystemConfigurableEnvironmentFetcher systemConfigFetcher; + + @InjectMocks + SystemConfigFirstExternalUrlSupplier externalUrl; + + @Nested + class HaloPropertiesSupplier { + + @BeforeEach + void setUp() { + when(systemConfigFetcher.getBasic()).thenReturn(Mono.empty()); + externalUrl.onExtensionInitialized(null); + } + + @Test + void getURIWhenUsingAbsolutePermalink() throws MalformedURLException { + var fakeUri = URI.create("https://halo.run/fake"); + when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); + when(haloProperties.isUseAbsolutePermalink()).thenReturn(true); + + assertEquals(fakeUri, externalUrl.get()); + } + + @Test + void getURIWhenBasePathSetAndNotUsingAbsolutePermalink() throws MalformedURLException { + when(webFluxProperties.getBasePath()).thenReturn("/blog"); + when(haloProperties.isUseAbsolutePermalink()).thenReturn(false); + + assertEquals(URI.create("/blog"), externalUrl.get()); + } + + @Test + void getURIWhenBasePathSetAndUsingAbsolutePermalink() throws MalformedURLException { + var fakeUri = URI.create("https://halo.run/fake"); + when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); + lenient().when(webFluxProperties.getBasePath()).thenReturn("/blog"); + when(haloProperties.isUseAbsolutePermalink()).thenReturn(true); + + assertEquals(URI.create("https://halo.run/fake"), externalUrl.get()); + } + + + @Test + void getURIWhenUsingRelativePermalink() throws MalformedURLException { + when(haloProperties.isUseAbsolutePermalink()).thenReturn(false); + + assertEquals(URI.create("/"), externalUrl.get()); + } + + @Test + void getURLWhenExternalURLProvided() throws MalformedURLException { + var fakeUri = URI.create("https://halo.run/fake"); + when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); + var mockRequest = mock(HttpRequest.class); + var url = externalUrl.getURL(mockRequest); + assertEquals(fakeUri.toURL(), url); + } + + @Test + void getURLWhenExternalURLAbsent() throws MalformedURLException { + var fakeUri = URI.create("https://localhost/fake"); + when(haloProperties.getExternalUrl()).thenReturn(null); + var mockRequest = mock(HttpRequest.class); + when(mockRequest.getURI()).thenReturn(fakeUri); + var url = externalUrl.getURL(mockRequest); + assertEquals(new URL("https://localhost/"), url); + } + + @Test + void getURLWhenBasePathSetAndExternalURLProvided() throws MalformedURLException { + var fakeUri = URI.create("https://localhost/fake"); + when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); + lenient().when(webFluxProperties.getBasePath()).thenReturn("/blog"); + var mockRequest = mock(HttpRequest.class); + lenient().when(mockRequest.getURI()).thenReturn(fakeUri); + var url = externalUrl.getURL(mockRequest); + assertEquals(new URL("https://localhost/fake"), url); + } + + @Test + void getURLWhenBasePathSetAndExternalURLAbsent() throws MalformedURLException { + var fakeUri = URI.create("https://localhost/fake"); + when(haloProperties.getExternalUrl()).thenReturn(null); + when(webFluxProperties.getBasePath()).thenReturn("/blog"); + var mockRequest = mock(HttpRequest.class); + when(mockRequest.getURI()).thenReturn(fakeUri); + var url = externalUrl.getURL(mockRequest); + assertEquals(new URL("https://localhost/blog"), url); + } + + @Test + void getRaw() throws MalformedURLException { + var fakeUri = URI.create("http://localhost/fake"); + when(haloProperties.getExternalUrl()).thenReturn(fakeUri.toURL()); + assertEquals(fakeUri.toURL(), externalUrl.getRaw()); + + when(haloProperties.getExternalUrl()).thenReturn(null); + assertNull(externalUrl.getRaw()); + } + + } + + @Nested + class SystemConfigSupplier { + + @Test + void shouldGetUrlCorrectly() throws Exception { + var basic = new SystemSetting.Basic(); + basic.setExternalUrl("https://www.halo.run"); + when(systemConfigFetcher.getBasic()).thenReturn(Mono.just(basic)); + externalUrl.onExtensionInitialized(null); + assertEquals(URI.create("https://www.halo.run").toURL(), externalUrl.getRaw()); + assertEquals(URI.create("https://www.halo.run"), externalUrl.get()); + + var mockRequest = mock(HttpRequest.class); + assertEquals(URI.create("https://www.halo.run").toURL(), + externalUrl.getURL(mockRequest)); + } + + } + +} \ No newline at end of file