mirror of https://github.com/halo-dev/halo
Merge branch 'main' into feature-user-center
commit
d356b65671
|
@ -1,15 +1,20 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'java-library'
|
id 'java-library'
|
||||||
id 'halo.publish'
|
id 'halo.publish'
|
||||||
id "io.freefair.lombok" version "8.0.0-rc2"
|
id "io.freefair.lombok" version "8.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'run.halo.app'
|
group = 'run.halo.app'
|
||||||
description = 'API of halo project, connecting by other projects.'
|
description = 'API of halo project, connecting by other projects.'
|
||||||
|
|
||||||
|
compileJava.options.encoding = "UTF-8"
|
||||||
|
compileTestJava.options.encoding = "UTF-8"
|
||||||
|
javadoc.options.encoding = "UTF-8"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url 'https://repo.spring.io/milestone' }
|
maven { url 'https://repo.spring.io/milestone' }
|
||||||
|
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' }
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'org.springframework.boot' version '3.1.5'
|
id 'org.springframework.boot' version '3.2.0-RC2'
|
||||||
id 'io.spring.dependency-management' version '1.1.0'
|
id 'io.spring.dependency-management' version '1.1.0'
|
||||||
id "com.gorylenko.gradle-git-properties" version "2.3.2"
|
id "com.gorylenko.gradle-git-properties" version "2.3.2"
|
||||||
id "checkstyle"
|
id "checkstyle"
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'jacoco'
|
id 'jacoco'
|
||||||
id "de.undercouch.download" version "5.3.1"
|
id "de.undercouch.download" version "5.3.1"
|
||||||
id "io.freefair.lombok" version "8.0.0-rc2"
|
id "io.freefair.lombok" version "8.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "run.halo.app"
|
group = "run.halo.app"
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
compileJava.options.encoding = "UTF-8"
|
||||||
|
compileTestJava.options.encoding = "UTF-8"
|
||||||
|
|
||||||
checkstyle {
|
checkstyle {
|
||||||
toolVersion = "9.3"
|
toolVersion = "9.3"
|
||||||
|
@ -22,6 +24,7 @@ repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|
||||||
maven { url 'https://repo.spring.io/milestone' }
|
maven { url 'https://repo.spring.io/milestone' }
|
||||||
|
maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ public interface ReplyService {
|
||||||
reply -> reply.getMetadata().getCreationTimestamp();
|
reply -> reply.getMetadata().getCreationTimestamp();
|
||||||
// ascending order by creation time
|
// ascending order by creation time
|
||||||
// asc nulls high will be placed at the end
|
// asc nulls high will be placed at the end
|
||||||
return Comparator.comparing(creationTime, Comparators.nullsHigh())
|
return Comparator.comparing(creationTime, Comparators.nullsLow())
|
||||||
.thenComparing(metadataCreationTime)
|
.thenComparing(metadataCreationTime)
|
||||||
.thenComparing(reply -> reply.getMetadata().getName());
|
.thenComparing(reply -> reply.getMetadata().getName());
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,6 +273,7 @@ public class AttachmentEndpoint implements CustomEndpoint {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Schema(types = "object")
|
||||||
public interface IUploadRequest {
|
public interface IUploadRequest {
|
||||||
|
|
||||||
@Schema(requiredMode = REQUIRED, description = "Attachment file")
|
@Schema(requiredMode = REQUIRED, description = "Attachment file")
|
||||||
|
|
|
@ -673,7 +673,7 @@ public class PluginEndpoint implements CustomEndpoint {
|
||||||
.flatMap(listResult -> ServerResponse.ok().bodyValue(listResult));
|
.flatMap(listResult -> ServerResponse.ok().bodyValue(listResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Schema(name = "PluginInstallRequest")
|
@Schema(name = "PluginInstallRequest", types = "object")
|
||||||
public static class InstallRequest {
|
public static class InstallRequest {
|
||||||
|
|
||||||
private final MultiValueMap<String, Part> multipartData;
|
private final MultiValueMap<String, Part> multipartData;
|
||||||
|
|
|
@ -8,7 +8,7 @@ import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuil
|
||||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springdoc.core.fn.builders.schema.Builder;
|
import org.springdoc.core.fn.builders.schema.Builder;
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
|
@ -42,9 +42,10 @@ import run.halo.app.extension.router.QueryParamBuildUtil;
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@AllArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class PostEndpoint implements CustomEndpoint {
|
public class PostEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
|
private int maxAttemptsWaitForPublish = 10;
|
||||||
private final PostService postService;
|
private final PostService postService;
|
||||||
private final ReactiveExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
|
@ -243,7 +244,7 @@ public class PostEndpoint implements CustomEndpoint {
|
||||||
})
|
})
|
||||||
.switchIfEmpty(Mono.error(
|
.switchIfEmpty(Mono.error(
|
||||||
() -> new RetryException("Retry to check post publish status"))))
|
() -> new RetryException("Retry to check post publish status"))))
|
||||||
.retryWhen(Retry.fixedDelay(10, Duration.ofMillis(200))
|
.retryWhen(Retry.backoff(maxAttemptsWaitForPublish, Duration.ofMillis(100))
|
||||||
.filter(t -> t instanceof RetryException));
|
.filter(t -> t instanceof RetryException));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -278,4 +279,11 @@ public class PostEndpoint implements CustomEndpoint {
|
||||||
return postService.listPost(postQuery)
|
return postService.listPost(postQuery)
|
||||||
.flatMap(listedPosts -> ServerResponse.ok().bodyValue(listedPosts));
|
.flatMap(listedPosts -> ServerResponse.ok().bodyValue(listedPosts));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenient for testing, to avoid waiting too long for post published when testing.
|
||||||
|
*/
|
||||||
|
public void setMaxAttemptsWaitForPublish(int maxAttempts) {
|
||||||
|
this.maxAttemptsWaitForPublish = maxAttempts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -244,6 +244,7 @@ public class UserEndpoint implements CustomEndpoint {
|
||||||
.flatMap(user -> ServerResponse.ok().bodyValue(user));
|
.flatMap(user -> ServerResponse.ok().bodyValue(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Schema(types = "object")
|
||||||
public interface IAvatarUploadRequest {
|
public interface IAvatarUploadRequest {
|
||||||
@Schema(requiredMode = REQUIRED, description = "Avatar file")
|
@Schema(requiredMode = REQUIRED, description = "Avatar file")
|
||||||
FilePart getFile();
|
FilePart getFile();
|
||||||
|
|
|
@ -8,7 +8,10 @@ import static run.halo.app.plugin.PluginConst.PLUGIN_PATH;
|
||||||
import static run.halo.app.plugin.PluginConst.RELOAD_ANNO;
|
import static run.halo.app.plugin.PluginConst.RELOAD_ANNO;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
@ -40,6 +43,7 @@ import org.springframework.lang.Nullable;
|
||||||
import org.springframework.retry.support.RetryTemplate;
|
import org.springframework.retry.support.RetryTemplate;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
import org.springframework.web.util.UriComponentsBuilder;
|
import org.springframework.web.util.UriComponentsBuilder;
|
||||||
import run.halo.app.core.extension.Plugin;
|
import run.halo.app.core.extension.Plugin;
|
||||||
import run.halo.app.core.extension.ReverseProxy;
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
|
@ -649,11 +653,19 @@ public class PluginReconciler implements Reconciler<Request> {
|
||||||
if (StringUtils.isBlank(pathString)) {
|
if (StringUtils.isBlank(pathString)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String processedPathString = pathString;
|
try {
|
||||||
if (processedPathString.startsWith("file:")) {
|
var pathURL = new URL(pathString);
|
||||||
processedPathString = processedPathString.substring(7);
|
if (!ResourceUtils.isFileURL(pathURL)) {
|
||||||
|
throw new IllegalArgumentException("The path cannot be resolved to absolute file"
|
||||||
|
+ " path because it does not reside in the file system: "
|
||||||
|
+ pathString);
|
||||||
}
|
}
|
||||||
return Paths.get(processedPathString);
|
var pathURI = ResourceUtils.toURI(pathURL);
|
||||||
|
return Paths.get(pathURI);
|
||||||
|
} catch (MalformedURLException | URISyntaxException ignored) {
|
||||||
|
// the given path string is not a valid URL.
|
||||||
|
}
|
||||||
|
return Paths.get(pathString);
|
||||||
}
|
}
|
||||||
|
|
||||||
URI toUri(String pathString) {
|
URI toUri(String pathString) {
|
||||||
|
|
|
@ -466,7 +466,7 @@ public class ThemeEndpoint implements CustomEndpoint {
|
||||||
.bodyValue(theme));
|
.bodyValue(theme));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Schema(name = "ThemeInstallRequest")
|
@Schema(name = "ThemeInstallRequest", types = "object")
|
||||||
public static class InstallRequest {
|
public static class InstallRequest {
|
||||||
|
|
||||||
@Schema(hidden = true)
|
@Schema(hidden = true)
|
||||||
|
|
|
@ -9,6 +9,11 @@ public class ThemeProperties {
|
||||||
@Valid
|
@Valid
|
||||||
private final Initializer initializer = new Initializer();
|
private final Initializer initializer = new Initializer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the generator meta needs to be disabled.
|
||||||
|
*/
|
||||||
|
private boolean generatorMetaDisabled;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class Initializer {
|
public static class Initializer {
|
||||||
|
|
||||||
|
|
|
@ -140,6 +140,7 @@ public class MigrationEndpoint implements CustomEndpoint {
|
||||||
.switchIfEmpty(backupFileContent);
|
.switchIfEmpty(backupFileContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Schema(types = "object")
|
||||||
public static class RestoreRequest {
|
public static class RestoreRequest {
|
||||||
private final MultiValueMap<String, Part> multipart;
|
private final MultiValueMap<String, Part> multipart;
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,10 @@ import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.autoconfigure.web.WebProperties;
|
import org.springframework.boot.autoconfigure.web.WebProperties;
|
||||||
|
import org.springframework.boot.info.BuildProperties;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
import org.springframework.core.io.FileSystemResource;
|
||||||
|
@ -21,8 +24,10 @@ import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.ThemeRootGetter;
|
import run.halo.app.infra.ThemeRootGetter;
|
||||||
import run.halo.app.infra.utils.FileUtils;
|
import run.halo.app.infra.utils.FileUtils;
|
||||||
|
import run.halo.app.theme.dialect.GeneratorMetaProcessor;
|
||||||
import run.halo.app.theme.dialect.HaloSpringSecurityDialect;
|
import run.halo.app.theme.dialect.HaloSpringSecurityDialect;
|
||||||
import run.halo.app.theme.dialect.LinkExpressionObjectDialect;
|
import run.halo.app.theme.dialect.LinkExpressionObjectDialect;
|
||||||
|
import run.halo.app.theme.dialect.TemplateHeadProcessor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
|
@ -86,4 +91,12 @@ public class ThemeConfiguration {
|
||||||
ServerSecurityContextRepository securityContextRepository) {
|
ServerSecurityContextRepository securityContextRepository) {
|
||||||
return new HaloSpringSecurityDialect(securityContextRepository);
|
return new HaloSpringSecurityDialect(securityContextRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnProperty(name = "halo.theme.generator-meta-disabled",
|
||||||
|
havingValue = "false",
|
||||||
|
matchIfMissing = true)
|
||||||
|
TemplateHeadProcessor generatorMetaProcessor(ObjectProvider<BuildProperties> buildProperties) {
|
||||||
|
return new GeneratorMetaProcessor(buildProperties);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
package run.halo.app.theme.dialect;
|
||||||
|
|
||||||
|
import static org.thymeleaf.model.AttributeValueQuotes.DOUBLE;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.boot.info.BuildProperties;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.thymeleaf.context.ITemplateContext;
|
||||||
|
import org.thymeleaf.model.IModel;
|
||||||
|
import org.thymeleaf.processor.element.IElementModelStructureHandler;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Processor for generating generator meta.
|
||||||
|
* Set the order to 0 for removing the meta in later TemplateHeadProcessor.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Order(0)
|
||||||
|
public class GeneratorMetaProcessor implements TemplateHeadProcessor {
|
||||||
|
|
||||||
|
private final String generatorValue;
|
||||||
|
|
||||||
|
public GeneratorMetaProcessor(ObjectProvider<BuildProperties> buildProperties) {
|
||||||
|
this.generatorValue = "Halo " + buildProperties.stream().findFirst()
|
||||||
|
.map(BuildProperties::getVersion)
|
||||||
|
.orElse("Unknown");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> process(ITemplateContext context, IModel model,
|
||||||
|
IElementModelStructureHandler structureHandler) {
|
||||||
|
return Mono.fromRunnable(() -> {
|
||||||
|
var modelFactory = context.getModelFactory();
|
||||||
|
var generatorMeta = modelFactory.createStandaloneElementTag("meta",
|
||||||
|
Map.of("name", "generator", "content", generatorValue),
|
||||||
|
DOUBLE, false, true);
|
||||||
|
model.add(generatorMeta);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,7 +1,10 @@
|
||||||
package run.halo.app.theme.finders.impl;
|
package run.halo.app.theme.finders.impl;
|
||||||
|
|
||||||
|
|
||||||
|
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
|
||||||
|
|
||||||
import java.security.Principal;
|
import java.security.Principal;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -9,7 +12,6 @@ import java.util.function.Function;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
import org.apache.commons.lang3.ObjectUtils;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
|
@ -254,35 +256,41 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService
|
||||||
public int compare(Comment c1, Comment c2) {
|
public int compare(Comment c1, Comment c2) {
|
||||||
boolean c1Top = BooleanUtils.isTrue(c1.getSpec().getTop());
|
boolean c1Top = BooleanUtils.isTrue(c1.getSpec().getTop());
|
||||||
boolean c2Top = BooleanUtils.isTrue(c2.getSpec().getTop());
|
boolean c2Top = BooleanUtils.isTrue(c2.getSpec().getTop());
|
||||||
if (c1Top == c2Top) {
|
|
||||||
int c1Priority = ObjectUtils.defaultIfNull(c1.getSpec().getPriority(), 0);
|
// c1 top = true && c2 top = false
|
||||||
int c2Priority = ObjectUtils.defaultIfNull(c2.getSpec().getPriority(), 0);
|
if (c1Top && !c2Top) {
|
||||||
if (c1Top) {
|
return -1;
|
||||||
// 都置顶
|
|
||||||
return Integer.compare(c1Priority, c2Priority);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 两个评论不置顶根据 creationTime 降序排列
|
// c1 top = false && c2 top = true
|
||||||
return Comparator.comparing(
|
if (!c1Top && c2Top) {
|
||||||
(Comment comment) -> comment.getSpec().getCreationTime(),
|
|
||||||
Comparators.nullsLow())
|
|
||||||
.thenComparing((Comment comment) -> comment.getMetadata().getName())
|
|
||||||
.compare(c2, c1);
|
|
||||||
} else if (c1Top) {
|
|
||||||
// 只有 c1 置顶,c1 排前面
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
// 只有c2置顶, c2排在前面
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// c1 top = c2 top = true || c1 top = c2 top = false
|
||||||
|
var priorityComparator = Comparator.<Comment, Integer>comparing(
|
||||||
|
comment -> defaultIfNull(comment.getSpec().getPriority(), 0));
|
||||||
|
|
||||||
|
var creationTimeComparator = Comparator.<Comment, Instant>comparing(
|
||||||
|
comment -> comment.getSpec().getCreationTime(),
|
||||||
|
Comparators.nullsLow(Comparator.<Instant>reverseOrder()));
|
||||||
|
|
||||||
|
var nameComparator = Comparator.<Comment, String>comparing(
|
||||||
|
comment -> comment.getMetadata().getName());
|
||||||
|
|
||||||
|
if (c1Top) {
|
||||||
|
return priorityComparator.thenComparing(creationTimeComparator)
|
||||||
|
.thenComparing(nameComparator)
|
||||||
|
.compare(c1, c2);
|
||||||
|
}
|
||||||
|
return creationTimeComparator.thenComparing(nameComparator).compare(c1, c2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int pageNullSafe(Integer page) {
|
int pageNullSafe(Integer page) {
|
||||||
return ObjectUtils.defaultIfNull(page, 1);
|
return defaultIfNull(page, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
int sizeNullSafe(Integer size) {
|
int sizeNullSafe(Integer size) {
|
||||||
return ObjectUtils.defaultIfNull(size, DEFAULT_SIZE);
|
return defaultIfNull(size, DEFAULT_SIZE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.config;
|
package run.halo.app.config;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.hamcrest.core.StringStartsWith;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -30,7 +31,8 @@ class WebFluxConfigTest {
|
||||||
.forEach(uri -> webClient.get().uri(uri)
|
.forEach(uri -> webClient.get().uri(uri)
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
.expectBody(String.class).isEqualTo("console index\n"));
|
.expectBody(String.class).value(StringStartsWith.startsWith("console index"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -38,7 +40,7 @@ class WebFluxConfigTest {
|
||||||
webClient.get().uri("/console/assets/fake.txt")
|
webClient.get().uri("/console/assets/fake.txt")
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
.expectBody(String.class).isEqualTo("fake.\n");
|
.expectBody(String.class).value(StringStartsWith.startsWith("fake."));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -50,6 +50,7 @@ class PostEndpointTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
|
postEndpoint.setMaxAttemptsWaitForPublish(3);
|
||||||
webTestClient = WebTestClient
|
webTestClient = WebTestClient
|
||||||
.bindToRouterFunction(postEndpoint.endpoint())
|
.bindToRouterFunction(postEndpoint.endpoint())
|
||||||
.build();
|
.build();
|
||||||
|
@ -170,7 +171,7 @@ class PostEndpointTest {
|
||||||
.is5xxServerError();
|
.is5xxServerError();
|
||||||
|
|
||||||
// Verify WebClient retry behavior
|
// Verify WebClient retry behavior
|
||||||
verify(client, times(12)).get(eq(Post.class), eq("post-1"));
|
verify(client, times(5)).get(eq(Post.class), eq("post-1"));
|
||||||
verify(client).update(any(Post.class));
|
verify(client).update(any(Post.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ package run.halo.app.core.extension.reconciler;
|
||||||
import static java.util.Objects.requireNonNull;
|
import static java.util.Objects.requireNonNull;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
@ -369,12 +371,15 @@ class PluginReconcilerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolvePluginPathAnnotation() {
|
void resolvePluginPathAnnotation() {
|
||||||
when(haloPluginManager.getPluginsRoot()).thenReturn(Paths.get("/tmp/plugins"));
|
var pluginRoot = Paths.get("tmp", "plugins");
|
||||||
String path = pluginReconciler.resolvePluginPathForAnno("/tmp/plugins/sitemap-1.0.jar");
|
when(haloPluginManager.getPluginsRoot()).thenReturn(pluginRoot);
|
||||||
|
var path = pluginReconciler.resolvePluginPathForAnno(
|
||||||
|
pluginRoot.resolve("sitemap-1.0.jar").toString());
|
||||||
assertThat(path).isEqualTo("sitemap-1.0.jar");
|
assertThat(path).isEqualTo("sitemap-1.0.jar");
|
||||||
|
|
||||||
path = pluginReconciler.resolvePluginPathForAnno("/abc/plugins/sitemap-1.0.jar");
|
var givenPath = Paths.get("abc", "plugins", "sitemap-1.0.jar");
|
||||||
assertThat(path).isEqualTo("/abc/plugins/sitemap-1.0.jar");
|
path = pluginReconciler.resolvePluginPathForAnno(givenPath.toString());
|
||||||
|
assertThat(path).isEqualTo(givenPath.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@ -457,17 +462,18 @@ class PluginReconcilerTest {
|
||||||
assertThat(pluginReconciler.toPath("")).isNull();
|
assertThat(pluginReconciler.toPath("")).isNull();
|
||||||
assertThat(pluginReconciler.toPath(" ")).isNull();
|
assertThat(pluginReconciler.toPath(" ")).isNull();
|
||||||
|
|
||||||
Path path = pluginReconciler.toPath("file:///path/to/file.txt");
|
final var filePath = Paths.get("path", "to", "file.txt").toAbsolutePath();
|
||||||
assertThat(path).isNotNull();
|
|
||||||
assertThat(path.toString()).isEqualTo("/path/to/file.txt");
|
|
||||||
|
|
||||||
assertThat(pluginReconciler.toPath("C:\\Users\\faker\\halo\\plugins").toString())
|
// test for file:///
|
||||||
.isEqualTo("C:\\Users\\faker\\halo\\plugins");
|
assertEquals(filePath, pluginReconciler.toPath(filePath.toUri().toString()));
|
||||||
assertThat(pluginReconciler.toPath("C:/Users/faker/halo/plugins").toString())
|
// test for absolute path /home/xyz or C:\Windows
|
||||||
.isEqualTo("C:/Users/faker/halo/plugins");
|
assertEquals(filePath, pluginReconciler.toPath(filePath.toString()));
|
||||||
Path windowsPath = Paths.get("C:/Users/username/Documents/file.txt");
|
|
||||||
assertThat(pluginReconciler.toPath("file://C:/Users/username/Documents/file.txt"))
|
var exception = assertThrows(IllegalArgumentException.class, () -> {
|
||||||
.isEqualTo(windowsPath);
|
var fileUri = filePath.toUri();
|
||||||
|
pluginReconciler.toPath(fileUri.toString().replaceFirst("file", "http"));
|
||||||
|
});
|
||||||
|
assertTrue(exception.getMessage().contains("not reside in the file system"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -483,8 +489,9 @@ class PluginReconcilerTest {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Test with non-empty pathString
|
// Test with non-empty pathString
|
||||||
URI uri = pluginReconciler.toUri("/path/to/file");
|
var filePath = Paths.get("path", "to", "file");
|
||||||
Assertions.assertEquals("file:///path/to/file", uri.toString());
|
URI uri = pluginReconciler.toUri(filePath.toString());
|
||||||
|
assertEquals(filePath.toUri(), uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -207,8 +207,9 @@ class PluginServiceImplTest {
|
||||||
String pluginName = "test-plugin";
|
String pluginName = "test-plugin";
|
||||||
PluginWrapper pluginWrapper = mock(PluginWrapper.class);
|
PluginWrapper pluginWrapper = mock(PluginWrapper.class);
|
||||||
when(pluginManager.getPlugin(pluginName)).thenReturn(pluginWrapper);
|
when(pluginManager.getPlugin(pluginName)).thenReturn(pluginWrapper);
|
||||||
|
var pluginPath = Paths.get("tmp", "plugins", "fake-plugin.jar");
|
||||||
when(pluginWrapper.getPluginPath())
|
when(pluginWrapper.getPluginPath())
|
||||||
.thenReturn(Paths.get("/tmp/plugins/fake-plugin.jar"));
|
.thenReturn(pluginPath);
|
||||||
Plugin plugin = new Plugin();
|
Plugin plugin = new Plugin();
|
||||||
plugin.setMetadata(new Metadata());
|
plugin.setMetadata(new Metadata());
|
||||||
plugin.getMetadata().setName(pluginName);
|
plugin.getMetadata().setName(pluginName);
|
||||||
|
@ -224,7 +225,7 @@ class PluginServiceImplTest {
|
||||||
verify(client, times(1)).update(
|
verify(client, times(1)).update(
|
||||||
argThat(p -> {
|
argThat(p -> {
|
||||||
String reloadPath = p.getMetadata().getAnnotations().get(PluginConst.RELOAD_ANNO);
|
String reloadPath = p.getMetadata().getAnnotations().get(PluginConst.RELOAD_ANNO);
|
||||||
assertThat(reloadPath).isEqualTo("/tmp/plugins/fake-plugin.jar");
|
assertThat(reloadPath).isEqualTo(pluginPath.toString());
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -64,8 +64,9 @@ class FileUtilsTest {
|
||||||
unzip(zis, unzipTarget);
|
unzip(zis, unzipTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
var content = Files.readString(unzipTarget.resolve("examplefile"));
|
var lines = Files.readAllLines(unzipTarget.resolve("examplefile"));
|
||||||
assertEquals("Here is an example file.\n", content);
|
assertEquals(1, lines.size());
|
||||||
|
assertEquals("Here is an example file.", lines.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -79,9 +80,9 @@ class FileUtilsTest {
|
||||||
try (var zis = new ZipInputStream(Files.newInputStream(zipPath))) {
|
try (var zis = new ZipInputStream(Files.newInputStream(zipPath))) {
|
||||||
unzip(zis, unzipTarget);
|
unzip(zis, unzipTarget);
|
||||||
}
|
}
|
||||||
|
var lines = Files.readAllLines(unzipTarget.resolve("examplefile"));
|
||||||
var content = Files.readString(unzipTarget.resolve("examplefile"));
|
assertEquals(1, lines.size());
|
||||||
assertEquals("Here is an example file.\n", content);
|
assertEquals("Here is an example file.", lines.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -23,7 +23,7 @@ class ThemeConfigurationTest {
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
private ThemeConfiguration themeConfiguration;
|
private ThemeConfiguration themeConfiguration;
|
||||||
|
|
||||||
private final Path themeRoot = Paths.get("/tmp/.halo/themes");
|
private final Path themeRoot = Paths.get("tmp", ".halo", "themes");
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
|
@ -33,25 +33,28 @@ class ThemeConfigurationTest {
|
||||||
@Test
|
@Test
|
||||||
void themeAssets() {
|
void themeAssets() {
|
||||||
Path path = themeConfiguration.getThemeAssetsPath("fake-theme", "hello.jpg");
|
Path path = themeConfiguration.getThemeAssetsPath("fake-theme", "hello.jpg");
|
||||||
assertThat(path).isEqualTo(themeRoot.resolve("fake-theme/templates/assets/hello.jpg"));
|
assertThat(path).isEqualTo(
|
||||||
|
themeRoot.resolve(Paths.get("fake-theme", "templates", "assets", "hello.jpg")));
|
||||||
|
|
||||||
path = themeConfiguration.getThemeAssetsPath("fake-theme", "./hello.jpg");
|
path = themeConfiguration.getThemeAssetsPath("fake-theme", "./hello.jpg");
|
||||||
assertThat(path).isEqualTo(themeRoot.resolve("fake-theme/templates/assets/./hello.jpg"));
|
assertThat(path).isEqualTo(
|
||||||
|
themeRoot.resolve(Paths.get("fake-theme", "templates", "assets", ".", "hello.jpg")));
|
||||||
|
|
||||||
assertThatThrownBy(() -> {
|
assertThatThrownBy(() ->
|
||||||
themeConfiguration.getThemeAssetsPath("fake-theme", "../../hello.jpg");
|
themeConfiguration.getThemeAssetsPath("fake-theme", "../../hello.jpg"))
|
||||||
}).isInstanceOf(AccessDeniedException.class)
|
.isInstanceOf(AccessDeniedException.class)
|
||||||
.hasMessage(
|
.hasMessageContaining("Directory traversal detected");
|
||||||
"403 FORBIDDEN \"Directory traversal detected: /tmp/"
|
|
||||||
+ ".halo/themes/fake-theme/templates/assets/../../hello.jpg\"");
|
|
||||||
|
|
||||||
path = themeConfiguration.getThemeAssetsPath("fake-theme", "%2e%2e/f.jpg");
|
path = themeConfiguration.getThemeAssetsPath("fake-theme", "%2e%2e/f.jpg");
|
||||||
assertThat(path).isEqualTo(themeRoot.resolve("fake-theme/templates/assets/%2e%2e/f.jpg"));
|
assertThat(path).isEqualTo(
|
||||||
|
themeRoot.resolve(Paths.get("fake-theme", "templates", "assets", "%2e%2e", "f.jpg")));
|
||||||
|
|
||||||
path = themeConfiguration.getThemeAssetsPath("fake-theme", "f/./../p.jpg");
|
path = themeConfiguration.getThemeAssetsPath("fake-theme", "f/./../p.jpg");
|
||||||
assertThat(path).isEqualTo(themeRoot.resolve("fake-theme/templates/assets/f/./../p.jpg"));
|
assertThat(path).isEqualTo(themeRoot.resolve(
|
||||||
|
Paths.get("fake-theme", "templates", "assets", "f", ".", "..", "p.jpg")));
|
||||||
|
|
||||||
path = themeConfiguration.getThemeAssetsPath("fake-theme", "f../p.jpg");
|
path = themeConfiguration.getThemeAssetsPath("fake-theme", "f../p.jpg");
|
||||||
assertThat(path).isEqualTo(themeRoot.resolve("fake-theme/templates/assets/f../p.jpg"));
|
assertThat(path).isEqualTo(
|
||||||
|
themeRoot.resolve(Paths.get("fake-theme", "templates", "assets", "f..", "p.jpg")));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
package run.halo.app.theme.dialect;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.infra.InitializationStateGetter;
|
||||||
|
import run.halo.app.theme.ThemeContext;
|
||||||
|
import run.halo.app.theme.ThemeResolver;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@AutoConfigureWebTestClient
|
||||||
|
class GeneratorMetaProcessorTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WebTestClient webClient;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
InitializationStateGetter initializationStateGetter;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
ThemeResolver themeResolver;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() throws FileNotFoundException, URISyntaxException {
|
||||||
|
when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));
|
||||||
|
var themeContext = ThemeContext.builder()
|
||||||
|
.name("default")
|
||||||
|
.path(Path.of(ResourceUtils.getURL("classpath:themes/default").toURI()))
|
||||||
|
.active(true)
|
||||||
|
.build();
|
||||||
|
when(themeResolver.getTheme(any(ServerWebExchange.class)))
|
||||||
|
.thenReturn(Mono.just(themeContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requestIndexPage() {
|
||||||
|
webClient.get().uri("/")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus().isOk()
|
||||||
|
.expectBody()
|
||||||
|
.consumeWith(System.out::println)
|
||||||
|
.xpath("/html/head/meta[@name=\"generator\"][starts-with(@content, \"Halo \")]")
|
||||||
|
.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -164,7 +164,7 @@ class CommentPublicQueryServiceImplTest {
|
||||||
.map(Comment::getMetadata)
|
.map(Comment::getMetadata)
|
||||||
.map(MetadataOperator::getName)
|
.map(MetadataOperator::getName)
|
||||||
.collect(Collectors.joining(", "));
|
.collect(Collectors.joining(", "));
|
||||||
assertThat(result).isEqualTo("1, 2, 3, 4, 5, 6, 9, 14, 10, 8, 7, 13, 12, 11");
|
assertThat(result).isEqualTo("1, 2, 4, 3, 5, 6, 10, 14, 9, 8, 7, 11, 12, 13");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -68,21 +68,9 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus()
|
.expectStatus()
|
||||||
.isOk()
|
.isOk()
|
||||||
.expectBody(String.class)
|
.expectBody()
|
||||||
.isEqualTo("""
|
.xpath("/html/body/div[1]").isEqualTo("zh")
|
||||||
<!DOCTYPE html>
|
.xpath("/html/body/div[2]").isEqualTo("欢迎来到首页");
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Title</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
index
|
|
||||||
<div>zh</div>
|
|
||||||
<div>欢迎来到首页</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -92,21 +80,9 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus()
|
.expectStatus()
|
||||||
.isOk()
|
.isOk()
|
||||||
.expectBody(String.class)
|
.expectBody()
|
||||||
.isEqualTo("""
|
.xpath("/html/body/div[1]").isEqualTo("en")
|
||||||
<!DOCTYPE html>
|
.xpath("/html/body/div[2]").isEqualTo("Welcome to the index");
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Title</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
index
|
|
||||||
<div>en</div>
|
|
||||||
<div>Welcome to the index</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -116,21 +92,9 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus()
|
.expectStatus()
|
||||||
.isOk()
|
.isOk()
|
||||||
.expectBody(String.class)
|
.expectBody()
|
||||||
.isEqualTo("""
|
.xpath("/html/body/div[1]").isEqualTo("foo")
|
||||||
<!DOCTYPE html>
|
.xpath("/html/body/div[2]").isEqualTo("欢迎来到首页");
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Title</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
index
|
|
||||||
<div>foo</div>
|
|
||||||
<div>欢迎来到首页</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -140,21 +104,11 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus()
|
.expectStatus()
|
||||||
.isOk()
|
.isOk()
|
||||||
.expectBody(String.class)
|
.expectBody()
|
||||||
.isEqualTo("""
|
.xpath("/html/head/title").isEqualTo("Title")
|
||||||
<!DOCTYPE html>
|
.xpath("/html/body/div[1]").isEqualTo("zh")
|
||||||
<html lang="en">
|
.xpath("/html/body/div[2]").isEqualTo("欢迎来到首页")
|
||||||
<head>
|
;
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Title</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
index
|
|
||||||
<div>zh</div>
|
|
||||||
<div>欢迎来到首页</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""");
|
|
||||||
|
|
||||||
// For other theme
|
// For other theme
|
||||||
when(themeResolver.getTheme(any(ServerWebExchange.class)))
|
when(themeResolver.getTheme(any(ServerWebExchange.class)))
|
||||||
|
@ -162,35 +116,16 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
webTestClient.get()
|
webTestClient.get()
|
||||||
.uri("/index?language=zh")
|
.uri("/index?language=zh")
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectBody(String.class)
|
.expectBody()
|
||||||
.isEqualTo("""
|
.xpath("/html/head/title").isEqualTo("Other theme title")
|
||||||
<!DOCTYPE html>
|
.xpath("/html/body/p").isEqualTo("Other 首页");
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Other theme title</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>Other 首页</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""");
|
|
||||||
webTestClient.get()
|
webTestClient.get()
|
||||||
.uri("/index?language=en")
|
.uri("/index?language=en")
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectBody(String.class)
|
.expectBody()
|
||||||
.isEqualTo("""
|
.xpath("/html/head/title").isEqualTo("Other theme title")
|
||||||
<!DOCTYPE html>
|
.xpath("/html/body/p").isEqualTo("other index");
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Other theme title</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<p>other index</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
""");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeContext createDefaultContext() throws URISyntaxException {
|
ThemeContext createDefaultContext() throws URISyntaxException {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8"/>
|
||||||
<title>Title</title>
|
<title>Title</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8"/>
|
||||||
<title>Other theme title</title>
|
<title>Other theme title</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
|
import type { EditorProvider, PluginModule } from "@halo-dev/console-shared";
|
||||||
import { onMounted, ref, type Ref, defineAsyncComponent } from "vue";
|
import { onMounted, ref, type Ref, defineAsyncComponent, markRaw } from "vue";
|
||||||
import { VLoading } from "@halo-dev/components";
|
import { VLoading } from "@halo-dev/components";
|
||||||
import Logo from "@/assets/logo.png";
|
import Logo from "@/assets/logo.png";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
|
@ -18,11 +18,13 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn {
|
||||||
{
|
{
|
||||||
name: "default",
|
name: "default",
|
||||||
displayName: t("core.plugin.extension_points.editor.providers.default"),
|
displayName: t("core.plugin.extension_points.editor.providers.default"),
|
||||||
component: defineAsyncComponent({
|
component: markRaw(
|
||||||
|
defineAsyncComponent({
|
||||||
loader: () => import("@/components/editor/DefaultEditor.vue"),
|
loader: () => import("@/components/editor/DefaultEditor.vue"),
|
||||||
loadingComponent: VLoading,
|
loadingComponent: VLoading,
|
||||||
delay: 200,
|
delay: 200,
|
||||||
}),
|
})
|
||||||
|
),
|
||||||
rawType: "HTML",
|
rawType: "HTML",
|
||||||
logo: Logo,
|
logo: Logo,
|
||||||
},
|
},
|
||||||
|
|
|
@ -21,7 +21,7 @@ export async function setupPluginModules(app: App) {
|
||||||
const { load } = useScriptTag(
|
const { load } = useScriptTag(
|
||||||
`${
|
`${
|
||||||
import.meta.env.VITE_API_URL
|
import.meta.env.VITE_API_URL
|
||||||
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js`
|
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.js?t=${Date.now()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
await load();
|
await load();
|
||||||
|
@ -45,7 +45,7 @@ export async function setupPluginModules(app: App) {
|
||||||
await loadStyle(
|
await loadStyle(
|
||||||
`${
|
`${
|
||||||
import.meta.env.VITE_API_URL
|
import.meta.env.VITE_API_URL
|
||||||
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css`
|
}/apis/api.console.halo.run/v1alpha1/plugins/-/bundle.css?t=${Date.now()}`
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = i18n.global.t("core.plugin.loader.toast.style_load_failed");
|
const message = i18n.global.t("core.plugin.loader.toast.style_load_failed");
|
||||||
|
|
|
@ -86,6 +86,7 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||||
import { usePluginModuleStore } from "@/stores/plugin";
|
import { usePluginModuleStore } from "@/stores/plugin";
|
||||||
import type { PluginModule } from "@halo-dev/console-shared";
|
import type { PluginModule } from "@halo-dev/console-shared";
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
|
import { onBeforeUnmount } from "vue";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -355,6 +356,10 @@ onMounted(() => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
editor.value?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
// image drag and paste upload
|
// image drag and paste upload
|
||||||
const { policies } = useFetchAttachmentPolicy();
|
const { policies } = useFetchAttachmentPolicy();
|
||||||
|
|
||||||
|
@ -491,6 +496,7 @@ const currentLocale = i18n.global.locale.value as
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div>
|
||||||
<AttachmentSelectorModal
|
<AttachmentSelectorModal
|
||||||
v-model:visible="attachmentSelectorModal"
|
v-model:visible="attachmentSelectorModal"
|
||||||
@select="onAttachmentSelect"
|
@select="onAttachmentSelect"
|
||||||
|
@ -690,4 +696,5 @@ const currentLocale = i18n.global.locale.value as
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
</template>
|
</template>
|
||||||
</RichTextEditor>
|
</RichTextEditor>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -17,10 +17,12 @@ import { randomUUID } from "@/utils/id";
|
||||||
const themeStore = useThemeStore();
|
const themeStore = useThemeStore();
|
||||||
|
|
||||||
function keyValidationRule(node: FormKitNode) {
|
function keyValidationRule(node: FormKitNode) {
|
||||||
return (
|
const validAnnotations = [
|
||||||
!annotations.value?.[node.value as string] &&
|
...Object.keys(annotations.value),
|
||||||
!customAnnotationsDuplicateKey.value
|
...customAnnotationsState.value.map((item) => item.key),
|
||||||
);
|
];
|
||||||
|
const count = validAnnotations.filter((item) => item === node.value);
|
||||||
|
return count.length < 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
@ -73,12 +75,6 @@ const annotations = ref<{
|
||||||
}>({});
|
}>({});
|
||||||
const customAnnotationsState = ref<{ key: string; value: string }[]>([]);
|
const customAnnotationsState = ref<{ key: string; value: string }[]>([]);
|
||||||
|
|
||||||
const customAnnotationsDuplicateKey = computed(() => {
|
|
||||||
const keys = customAnnotationsState.value.map((item) => item.key);
|
|
||||||
const uniqueKeys = new Set(keys);
|
|
||||||
return keys.length !== uniqueKeys.size;
|
|
||||||
});
|
|
||||||
|
|
||||||
const customAnnotations = computed(() => {
|
const customAnnotations = computed(() => {
|
||||||
return customAnnotationsState.value.reduce((acc, cur) => {
|
return customAnnotationsState.value.reduce((acc, cur) => {
|
||||||
acc[cur.key] = cur.value;
|
acc[cur.key] = cur.value;
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { VModal, IconLink } from "@halo-dev/components";
|
import {
|
||||||
|
VModal,
|
||||||
|
IconLink,
|
||||||
|
VTabbar,
|
||||||
|
IconComputer,
|
||||||
|
IconTablet,
|
||||||
|
IconPhone,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import { computed, markRaw, ref } from "vue";
|
||||||
|
|
||||||
withDefaults(
|
withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -25,6 +33,32 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
emit("close");
|
emit("close");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockDevices = [
|
||||||
|
{
|
||||||
|
id: "desktop",
|
||||||
|
icon: markRaw(IconComputer),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "tablet",
|
||||||
|
icon: markRaw(IconTablet),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "phone",
|
||||||
|
icon: markRaw(IconPhone),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const deviceActiveId = ref(mockDevices[0].id);
|
||||||
|
|
||||||
|
const iframeClasses = computed(() => {
|
||||||
|
if (deviceActiveId.value === "desktop") {
|
||||||
|
return "w-full h-full";
|
||||||
|
}
|
||||||
|
if (deviceActiveId.value === "tablet") {
|
||||||
|
return "w-2/3 h-2/3 ring-2 rounded ring-gray-300";
|
||||||
|
}
|
||||||
|
return "w-96 h-[50rem] ring-2 rounded ring-gray-300";
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<VModal
|
<VModal
|
||||||
|
@ -35,6 +69,14 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
:layer-closable="true"
|
:layer-closable="true"
|
||||||
@update:visible="onVisibleChange"
|
@update:visible="onVisibleChange"
|
||||||
>
|
>
|
||||||
|
<template #center>
|
||||||
|
<!-- TODO: Reactor VTabbar component to support icon prop -->
|
||||||
|
<VTabbar
|
||||||
|
v-model:active-id="deviceActiveId"
|
||||||
|
:items="mockDevices as any"
|
||||||
|
type="outline"
|
||||||
|
></VTabbar>
|
||||||
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
<span>
|
<span>
|
||||||
|
@ -46,7 +88,8 @@ const onVisibleChange = (visible: boolean) => {
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<iframe
|
<iframe
|
||||||
v-if="visible"
|
v-if="visible"
|
||||||
class="h-full w-full border-none transition-all duration-300"
|
class="border-none transition-all duration-500"
|
||||||
|
:class="iframeClasses"
|
||||||
:src="url"
|
:src="url"
|
||||||
></iframe>
|
></iframe>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@ export function loadStyle(href: string) {
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
let shouldAppend = false;
|
let shouldAppend = false;
|
||||||
let el: HTMLLinkElement | null = document.querySelector(
|
let el: HTMLLinkElement | null = document.querySelector(
|
||||||
'script[src="' + href + '"]'
|
'link[href="' + href + '"]'
|
||||||
);
|
);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
el = document.createElement("link");
|
el = document.createElement("link");
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
version=2.11.0-SNAPSHOT
|
version=2.11.0-SNAPSHOT
|
||||||
|
snakeyaml.version=2.2
|
||||||
|
|
Binary file not shown.
|
@ -1,6 +1,6 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import org.springframework.boot.gradle.plugin.SpringBootPlugin
|
import org.springframework.boot.gradle.plugin.SpringBootPlugin
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'org.springframework.boot' version '3.1.5' apply false
|
id 'org.springframework.boot' version '3.2.0-RC2' apply false
|
||||||
id 'java-platform'
|
id 'java-platform'
|
||||||
id 'halo.publish'
|
id 'halo.publish'
|
||||||
id 'signing'
|
id 'signing'
|
||||||
|
@ -18,7 +18,7 @@ ext {
|
||||||
guava = "32.0.1-jre"
|
guava = "32.0.1-jre"
|
||||||
jsoup = '1.15.3'
|
jsoup = '1.15.3'
|
||||||
jsonPatch = "1.13"
|
jsonPatch = "1.13"
|
||||||
springDocOpenAPI = "2.2.0"
|
springDocOpenAPI = "2.2.1-SNAPSHOT"
|
||||||
lucene = "9.7.0"
|
lucene = "9.7.0"
|
||||||
resilience4jVersion = "2.0.2"
|
resilience4jVersion = "2.0.2"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue