From a94c0c7f854c7a0e30508dd787a1c5cad341dfc1 Mon Sep 17 00:00:00 2001 From: John Niang Date: Wed, 19 Apr 2023 15:54:24 +0800 Subject: [PATCH] Add property halo.use-absolute-permalink to control permalink generation (#3772) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /kind api-change /area core /milestone 2.5.x #### What this PR does / why we need it: Add property `halo.use-absolute-permalink`(default is `false`) to control permalink generation. Leave `halo.external-url` as `null` by default. Meanwhile, I enhanced `ExternalUrlSupplier#getURL` to get URL from not only properties but only http request. #### How to use it? ```yaml halo: use-absolute-permalink: false ``` Or: ```yaml halo: external-url: https://halo.run/ use-absolute-permalink: false ``` Or: ```yaml halo: external-url: https://halo.run/ use-absolute-permalink: true ``` #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/3762 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note 新增是否生成相对地址的配置 ``` --- .../halo/app/infra/ExternalUrlSupplier.java | 20 +++++ .../halo/app/actuator/GlobalInfoEndpoint.java | 13 +-- .../HaloPropertiesExternalUrlSupplier.java | 43 ++++++++- .../app/infra/properties/HaloProperties.java | 31 ++++++- .../theme/dialect/HaloTrackerProcessor.java | 10 +-- .../src/main/resources/application.yaml | 1 - ...HaloPropertiesExternalUrlSupplierTest.java | 87 ++++++++++++++++++- console/src/locales/en.yaml | 3 + console/src/locales/zh-CN.yaml | 3 + console/src/locales/zh-TW.yaml | 3 + .../src/modules/system/actuator/Actuator.vue | 10 ++- .../modules/system/actuator/types/index.ts | 1 + 12 files changed, 203 insertions(+), 22 deletions(-) diff --git a/api/src/main/java/run/halo/app/infra/ExternalUrlSupplier.java b/api/src/main/java/run/halo/app/infra/ExternalUrlSupplier.java index 26bf8bb17..fa5bc74e8 100644 --- a/api/src/main/java/run/halo/app/infra/ExternalUrlSupplier.java +++ b/api/src/main/java/run/halo/app/infra/ExternalUrlSupplier.java @@ -1,7 +1,9 @@ package run.halo.app.infra; import java.net.URI; +import java.net.URL; import java.util.function.Supplier; +import org.springframework.http.HttpRequest; /** * Represents a supplier of external url configuration. @@ -10,4 +12,22 @@ import java.util.function.Supplier; */ public interface ExternalUrlSupplier extends Supplier { + /** + * Gets URI according to external URL and use-absolute-permalink properties. + * + * @return URI "/" returned if use-absolute-permalink is false. Or external URL will be + * returned.(never null) + */ + @Override + URI get(); + + /** + * Gets URL according to external URL and server request URL. + * + * @param request represents an HTTP request message, consisting of a method and a URI. + * @return External URL will be return if it is provided, or request URI will be returned. + * (never null) + */ + URL getURL(HttpRequest request); + } diff --git a/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java b/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java index b835bf49d..9ed591f18 100644 --- a/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java +++ b/application/src/main/java/run/halo/app/actuator/GlobalInfoEndpoint.java @@ -2,7 +2,7 @@ package run.halo.app.actuator; import static org.apache.commons.lang3.BooleanUtils.isTrue; -import java.net.URI; +import java.net.URL; import java.util.List; import java.util.Locale; import java.util.TimeZone; @@ -14,12 +14,12 @@ import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint; import org.springframework.stereotype.Component; import run.halo.app.extension.ConfigMap; -import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; import run.halo.app.infra.SystemSetting.Basic; import run.halo.app.infra.SystemSetting.Comment; import run.halo.app.infra.SystemSetting.User; +import run.halo.app.infra.properties.HaloProperties; import run.halo.app.security.AuthProviderService; @WebEndpoint(id = "globalinfo") @@ -29,14 +29,15 @@ public class GlobalInfoEndpoint { private final ObjectProvider systemConfigFetcher; - private final ExternalUrlSupplier externalUrl; + private final HaloProperties haloProperties; private final AuthProviderService authProviderService; @ReadOperation public GlobalInfo globalInfo() { final var info = new GlobalInfo(); - info.setExternalUrl(externalUrl.get()); + info.setExternalUrl(haloProperties.getExternalUrl()); + info.setUseAbsolutePermalink(haloProperties.isUseAbsolutePermalink()); info.setLocale(Locale.getDefault()); info.setTimeZone(TimeZone.getDefault()); handleSocialAuthProvider(info); @@ -53,7 +54,9 @@ public class GlobalInfoEndpoint { @Data public static class GlobalInfo { - private URI externalUrl; + private URL externalUrl; + + private boolean useAbsolutePermalink; private TimeZone timeZone; diff --git a/application/src/main/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplier.java b/application/src/main/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplier.java index 15d805be8..453b89adc 100644 --- a/application/src/main/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplier.java +++ b/application/src/main/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplier.java @@ -1,7 +1,13 @@ 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.stereotype.Component; +import org.springframework.util.StringUtils; import run.halo.app.infra.properties.HaloProperties; /** @@ -14,12 +20,45 @@ public class HaloPropertiesExternalUrlSupplier implements ExternalUrlSupplier { private final HaloProperties haloProperties; - public HaloPropertiesExternalUrlSupplier(HaloProperties haloProperties) { + private final WebFluxProperties webFluxProperties; + + public HaloPropertiesExternalUrlSupplier(HaloProperties haloProperties, + WebFluxProperties webFluxProperties) { this.haloProperties = haloProperties; + this.webFluxProperties = webFluxProperties; } @Override public URI get() { - return haloProperties.getExternalUrl(); + 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; + } + + 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/properties/HaloProperties.java b/application/src/main/java/run/halo/app/infra/properties/HaloProperties.java index 8bfc1c7dc..0a03bf632 100644 --- a/application/src/main/java/run/halo/app/infra/properties/HaloProperties.java +++ b/application/src/main/java/run/halo/app/infra/properties/HaloProperties.java @@ -2,12 +2,14 @@ package run.halo.app.infra.properties; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; -import java.net.URI; +import java.net.URL; import java.nio.file.Path; import java.util.HashSet; import java.util.Set; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; /** @@ -17,13 +19,20 @@ import org.springframework.validation.annotation.Validated; @Data @ConfigurationProperties(prefix = "halo") @Validated -public class HaloProperties { +public class HaloProperties implements Validator { @NotNull private Path workDir; - @NotNull - private URI externalUrl; + /** + * External URL must be a URL and it can be null. + */ + private URL externalUrl; + + /** + * Indicates if we use absolute permalink to post, page, category, tag and so on. + */ + private boolean useAbsolutePermalink; private Set initialExtensionLocations = new HashSet<>(); @@ -48,4 +57,18 @@ public class HaloProperties { @Valid private final AttachmentProperties attachment = new AttachmentProperties(); + + @Override + public boolean supports(Class clazz) { + return HaloProperties.class.isAssignableFrom(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + var props = (HaloProperties) target; + if (props.isUseAbsolutePermalink() && props.getExternalUrl() == null) { + errors.rejectValue("externalUrl", "external-url.required.when-using-absolute-permalink", + "External URL is required when property `use-absolute-permalink` is set to true."); + } + } } diff --git a/application/src/main/java/run/halo/app/theme/dialect/HaloTrackerProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/HaloTrackerProcessor.java index 1e820e047..beb7d624c 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/HaloTrackerProcessor.java +++ b/application/src/main/java/run/halo/app/theme/dialect/HaloTrackerProcessor.java @@ -8,7 +8,7 @@ import org.thymeleaf.model.IModelFactory; import org.thymeleaf.processor.element.IElementModelStructureHandler; import reactor.core.publisher.Mono; import run.halo.app.extension.GroupVersionKind; -import run.halo.app.infra.properties.HaloProperties; +import run.halo.app.infra.ExternalUrlSupplier; import run.halo.app.infra.utils.PathUtils; /** @@ -21,10 +21,10 @@ import run.halo.app.infra.utils.PathUtils; @Component public class HaloTrackerProcessor implements TemplateHeadProcessor { - private final HaloProperties haloProperties; + private final ExternalUrlSupplier externalUrlGetter; - public HaloTrackerProcessor(HaloProperties haloProperties) { - this.haloProperties = haloProperties; + public HaloTrackerProcessor(ExternalUrlSupplier externalUrlGetter) { + this.externalUrlGetter = externalUrlGetter; } @Override @@ -42,7 +42,7 @@ public class HaloTrackerProcessor implements TemplateHeadProcessor { private String getTrackerScript(ITemplateContext context) { String resourceName = (String) context.getVariable("name"); - String externalUrl = haloProperties.getExternalUrl().getPath(); + String externalUrl = externalUrlGetter.get().getPath(); Object groupVersionKind = context.getVariable("groupVersionKind"); Object plural = context.getVariable("plural"); if (groupVersionKind == null || plural == null) { diff --git a/application/src/main/resources/application.yaml b/application/src/main/resources/application.yaml index 8c5a00893..2f1e1d8ba 100644 --- a/application/src/main/resources/application.yaml +++ b/application/src/main/resources/application.yaml @@ -29,7 +29,6 @@ spring: max-age: 365d halo: - external-url: "/" work-dir: ${user.home}/.halo2 plugin: plugins-root: ${halo.work-dir}/plugins diff --git a/application/src/test/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplierTest.java b/application/src/test/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplierTest.java index 2fc005211..b94ff1127 100644 --- a/application/src/test/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplierTest.java +++ b/application/src/test/java/run/halo/app/infra/HaloPropertiesExternalUrlSupplierTest.java @@ -1,14 +1,20 @@ package run.halo.app.infra; import static org.junit.jupiter.api.Assertions.assertEquals; +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.Mockito; 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) @@ -17,13 +23,86 @@ class HaloPropertiesExternalUrlSupplierTest { @Mock HaloProperties haloProperties; + @Mock + WebFluxProperties webFluxProperties; + @InjectMocks HaloPropertiesExternalUrlSupplier externalUrl; @Test - void get() { - URI fakeUri = URI.create("fake-url"); - Mockito.when(haloProperties.getExternalUrl()).thenReturn(fakeUri); + 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); + } + } \ No newline at end of file diff --git a/console/src/locales/en.yaml b/console/src/locales/en.yaml index 62bbf5a13..b4a7aa19a 100644 --- a/console/src/locales/en.yaml +++ b/console/src/locales/en.yaml @@ -930,6 +930,9 @@ core: database: Database os: Operating system log: System log + fields_values: + external_url: + not_setup: Not setup copy_results: external_url: "External url: {external_url}" start_time: "Start time: {start_time}" diff --git a/console/src/locales/zh-CN.yaml b/console/src/locales/zh-CN.yaml index 129a4b982..9b288c28b 100644 --- a/console/src/locales/zh-CN.yaml +++ b/console/src/locales/zh-CN.yaml @@ -930,6 +930,9 @@ core: database: 数据库 os: 操作系统 log: 运行日志 + fields_values: + external_url: + not_setup: 未设置 copy_results: external_url: 外部访问地址:{external_url} start_time: 启动时间:{start_time} diff --git a/console/src/locales/zh-TW.yaml b/console/src/locales/zh-TW.yaml index 8988541a4..b9f1172d1 100644 --- a/console/src/locales/zh-TW.yaml +++ b/console/src/locales/zh-TW.yaml @@ -930,6 +930,9 @@ core: database: 資料庫 os: 操作系統 log: 運行日誌 + fields_values: + external_url: + not_setup: 未設置 copy_results: external_url: 外部訪問地址:{external_url} start_time: 啟動時間:{start_time} diff --git a/console/src/modules/system/actuator/Actuator.vue b/console/src/modules/system/actuator/Actuator.vue index 743744243..aaf9dfdb4 100644 --- a/console/src/modules/system/actuator/Actuator.vue +++ b/console/src/modules/system/actuator/Actuator.vue @@ -52,9 +52,14 @@ const handleFetchActuatorStartup = async () => { }; const isExternalUrlValid = computed(() => { + if (!globalInfo.value?.useAbsolutePermalink) { + return true; + } + if (!globalInfo.value?.externalUrl) { return true; } + const url = new URL(globalInfo.value.externalUrl); const { host: currentHost, protocol: currentProtocol } = window.location; return url.host === currentHost && url.protocol === currentProtocol; @@ -162,9 +167,12 @@ const handleDownloadLogfile = () => { {{ $t("core.actuator.fields.external_url") }}
- + {{ globalInfo?.externalUrl }} + + {{ $t("core.actuator.fields_values.external_url.not_setup") }} +