mirror of https://github.com/halo-dev/halo
Add property halo.use-absolute-permalink to control permalink generation (#3772)
#### 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 新增是否生成相对地址的配置 ```pull/3747/head
parent
762747624f
commit
a94c0c7f85
|
@ -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<URI> {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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<SystemConfigurableEnvironmentFetcher> 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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -29,7 +29,6 @@ spring:
|
|||
max-age: 365d
|
||||
|
||||
halo:
|
||||
external-url: "/"
|
||||
work-dir: ${user.home}/.halo2
|
||||
plugin:
|
||||
plugins-root: ${halo.work-dir}/plugins
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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}"
|
||||
|
|
|
@ -930,6 +930,9 @@ core:
|
|||
database: 数据库
|
||||
os: 操作系统
|
||||
log: 运行日志
|
||||
fields_values:
|
||||
external_url:
|
||||
not_setup: 未设置
|
||||
copy_results:
|
||||
external_url: 外部访问地址:{external_url}
|
||||
start_time: 启动时间:{start_time}
|
||||
|
|
|
@ -930,6 +930,9 @@ core:
|
|||
database: 資料庫
|
||||
os: 操作系統
|
||||
log: 運行日誌
|
||||
fields_values:
|
||||
external_url:
|
||||
not_setup: 未設置
|
||||
copy_results:
|
||||
external_url: 外部訪問地址:{external_url}
|
||||
start_time: 啟動時間:{start_time}
|
||||
|
|
|
@ -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") }}
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||
<span>
|
||||
<span v-if="globalInfo?.externalUrl">
|
||||
{{ globalInfo?.externalUrl }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t("core.actuator.fields_values.external_url.not_setup") }}
|
||||
</span>
|
||||
<VAlert
|
||||
v-if="!isExternalUrlValid"
|
||||
class="mt-3"
|
||||
|
|
|
@ -6,6 +6,7 @@ export interface GlobalInfo {
|
|||
allowAnonymousComments: boolean;
|
||||
allowRegistration: boolean;
|
||||
socialAuthProviders: SocialAuthProvider[];
|
||||
useAbsolutePermalink: boolean;
|
||||
}
|
||||
|
||||
export interface Info {
|
||||
|
|
Loading…
Reference in New Issue