diff --git a/api/build.gradle b/api/build.gradle index 4e8a5c6cd..102a50883 100644 --- a/api/build.gradle +++ b/api/build.gradle @@ -1,15 +1,20 @@ plugins { id 'java-library' id 'halo.publish' - id "io.freefair.lombok" version "8.0.0-rc2" + id "io.freefair.lombok" version "8.4" } group = 'run.halo.app' 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 { mavenCentral() maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' } } dependencies { diff --git a/application/build.gradle b/application/build.gradle index 71baa8060..d26002f3e 100644 --- a/application/build.gradle +++ b/application/build.gradle @@ -1,16 +1,18 @@ 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 "com.gorylenko.gradle-git-properties" version "2.3.2" id "checkstyle" id 'java' id 'jacoco' 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" sourceCompatibility = JavaVersion.VERSION_17 +compileJava.options.encoding = "UTF-8" +compileTestJava.options.encoding = "UTF-8" checkstyle { toolVersion = "9.3" @@ -22,6 +24,7 @@ repositories { mavenCentral() maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://s01.oss.sonatype.org/content/repositories/snapshots' } } diff --git a/application/src/main/java/run/halo/app/content/comment/ReplyService.java b/application/src/main/java/run/halo/app/content/comment/ReplyService.java index 2010f229e..56962e870 100644 --- a/application/src/main/java/run/halo/app/content/comment/ReplyService.java +++ b/application/src/main/java/run/halo/app/content/comment/ReplyService.java @@ -31,7 +31,7 @@ public interface ReplyService { reply -> reply.getMetadata().getCreationTimestamp(); // ascending order by creation time // asc nulls high will be placed at the end - return Comparator.comparing(creationTime, Comparators.nullsHigh()) + return Comparator.comparing(creationTime, Comparators.nullsLow()) .thenComparing(metadataCreationTime) .thenComparing(reply -> reply.getMetadata().getName()); } diff --git a/application/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpoint.java b/application/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpoint.java index 4274507f0..d8b41c0f6 100644 --- a/application/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/attachment/endpoint/AttachmentEndpoint.java @@ -273,6 +273,7 @@ public class AttachmentEndpoint implements CustomEndpoint { } } + @Schema(types = "object") public interface IUploadRequest { @Schema(requiredMode = REQUIRED, description = "Attachment file") diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java index d6d015c1f..b85189765 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java @@ -673,7 +673,7 @@ public class PluginEndpoint implements CustomEndpoint { .flatMap(listResult -> ServerResponse.ok().bodyValue(listResult)); } - @Schema(name = "PluginInstallRequest") + @Schema(name = "PluginInstallRequest", types = "object") public static class InstallRequest { private final MultiValueMap multipartData; diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java index c15247bd6..b888db542 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/PostEndpoint.java @@ -8,7 +8,7 @@ import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuil import io.swagger.v3.oas.annotations.enums.ParameterIn; import java.time.Duration; import java.util.Objects; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springdoc.core.fn.builders.schema.Builder; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; @@ -42,9 +42,10 @@ import run.halo.app.extension.router.QueryParamBuildUtil; */ @Slf4j @Component -@AllArgsConstructor +@RequiredArgsConstructor public class PostEndpoint implements CustomEndpoint { + private int maxAttemptsWaitForPublish = 10; private final PostService postService; private final ReactiveExtensionClient client; @@ -243,7 +244,7 @@ public class PostEndpoint implements CustomEndpoint { }) .switchIfEmpty(Mono.error( () -> 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)); } @@ -278,4 +279,11 @@ public class PostEndpoint implements CustomEndpoint { return postService.listPost(postQuery) .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; + } } diff --git a/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java b/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java index 20faeb57c..47441a1d4 100644 --- a/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java @@ -244,6 +244,7 @@ public class UserEndpoint implements CustomEndpoint { .flatMap(user -> ServerResponse.ok().bodyValue(user)); } + @Schema(types = "object") public interface IAvatarUploadRequest { @Schema(requiredMode = REQUIRED, description = "Avatar file") FilePart getFile(); diff --git a/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java b/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java index 5361b08a8..0e02b6922 100644 --- a/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java +++ b/application/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java @@ -8,7 +8,10 @@ import static run.halo.app.plugin.PluginConst.PLUGIN_PATH; import static run.halo.app.plugin.PluginConst.RELOAD_ANNO; import java.io.IOException; +import java.net.MalformedURLException; import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -40,6 +43,7 @@ import org.springframework.lang.Nullable; import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.springframework.util.ResourceUtils; import org.springframework.web.util.UriComponentsBuilder; import run.halo.app.core.extension.Plugin; import run.halo.app.core.extension.ReverseProxy; @@ -649,11 +653,19 @@ public class PluginReconciler implements Reconciler { if (StringUtils.isBlank(pathString)) { return null; } - String processedPathString = pathString; - if (processedPathString.startsWith("file:")) { - processedPathString = processedPathString.substring(7); + try { + var pathURL = new URL(pathString); + 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); + } + 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(processedPathString); + return Paths.get(pathString); } URI toUri(String pathString) { diff --git a/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java b/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java index 233abad3e..2d96a3066 100644 --- a/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java +++ b/application/src/main/java/run/halo/app/core/extension/theme/ThemeEndpoint.java @@ -466,7 +466,7 @@ public class ThemeEndpoint implements CustomEndpoint { .bodyValue(theme)); } - @Schema(name = "ThemeInstallRequest") + @Schema(name = "ThemeInstallRequest", types = "object") public static class InstallRequest { @Schema(hidden = true) diff --git a/application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java b/application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java index b4decf3f3..7f5f11c91 100644 --- a/application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java +++ b/application/src/main/java/run/halo/app/infra/properties/ThemeProperties.java @@ -9,6 +9,11 @@ public class ThemeProperties { @Valid private final Initializer initializer = new Initializer(); + /** + * Indicates whether the generator meta needs to be disabled. + */ + private boolean generatorMetaDisabled; + @Data public static class Initializer { diff --git a/application/src/main/java/run/halo/app/migration/MigrationEndpoint.java b/application/src/main/java/run/halo/app/migration/MigrationEndpoint.java index 1bd43831a..33017defa 100644 --- a/application/src/main/java/run/halo/app/migration/MigrationEndpoint.java +++ b/application/src/main/java/run/halo/app/migration/MigrationEndpoint.java @@ -140,6 +140,7 @@ public class MigrationEndpoint implements CustomEndpoint { .switchIfEmpty(backupFileContent); } + @Schema(types = "object") public static class RestoreRequest { private final MultiValueMap multipart; diff --git a/application/src/main/java/run/halo/app/theme/ThemeConfiguration.java b/application/src/main/java/run/halo/app/theme/ThemeConfiguration.java index 1a921761b..08e81cbeb 100644 --- a/application/src/main/java/run/halo/app/theme/ThemeConfiguration.java +++ b/application/src/main/java/run/halo/app/theme/ThemeConfiguration.java @@ -8,7 +8,10 @@ import java.io.IOException; import java.nio.file.Path; import java.time.Instant; 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.info.BuildProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.FileSystemResource; @@ -21,8 +24,10 @@ import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect; import reactor.core.publisher.Mono; import run.halo.app.infra.ThemeRootGetter; 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.LinkExpressionObjectDialect; +import run.halo.app.theme.dialect.TemplateHeadProcessor; /** * @author guqing @@ -86,4 +91,12 @@ public class ThemeConfiguration { ServerSecurityContextRepository securityContextRepository) { return new HaloSpringSecurityDialect(securityContextRepository); } + + @Bean + @ConditionalOnProperty(name = "halo.theme.generator-meta-disabled", + havingValue = "false", + matchIfMissing = true) + TemplateHeadProcessor generatorMetaProcessor(ObjectProvider buildProperties) { + return new GeneratorMetaProcessor(buildProperties); + } } diff --git a/application/src/main/java/run/halo/app/theme/dialect/GeneratorMetaProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/GeneratorMetaProcessor.java new file mode 100644 index 000000000..ca2ee56cd --- /dev/null +++ b/application/src/main/java/run/halo/app/theme/dialect/GeneratorMetaProcessor.java @@ -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) { + this.generatorValue = "Halo " + buildProperties.stream().findFirst() + .map(BuildProperties::getVersion) + .orElse("Unknown"); + } + + @Override + public Mono 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); + }); + } + +} diff --git a/application/src/main/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImpl.java b/application/src/main/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImpl.java index 21c56609a..29eb383db 100644 --- a/application/src/main/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImpl.java +++ b/application/src/main/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImpl.java @@ -1,7 +1,10 @@ package run.halo.app.theme.finders.impl; +import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; + import java.security.Principal; +import java.time.Instant; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -9,7 +12,6 @@ import java.util.function.Function; import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.lang.Nullable; import org.springframework.security.core.context.ReactiveSecurityContextHolder; @@ -254,35 +256,41 @@ public class CommentPublicQueryServiceImpl implements CommentPublicQueryService public int compare(Comment c1, Comment c2) { boolean c1Top = BooleanUtils.isTrue(c1.getSpec().getTop()); boolean c2Top = BooleanUtils.isTrue(c2.getSpec().getTop()); - if (c1Top == c2Top) { - int c1Priority = ObjectUtils.defaultIfNull(c1.getSpec().getPriority(), 0); - int c2Priority = ObjectUtils.defaultIfNull(c2.getSpec().getPriority(), 0); - if (c1Top) { - // 都置顶 - return Integer.compare(c1Priority, c2Priority); - } - // 两个评论不置顶根据 creationTime 降序排列 - return Comparator.comparing( - (Comment comment) -> comment.getSpec().getCreationTime(), - Comparators.nullsLow()) - .thenComparing((Comment comment) -> comment.getMetadata().getName()) - .compare(c2, c1); - } else if (c1Top) { - // 只有 c1 置顶,c1 排前面 + // c1 top = true && c2 top = false + if (c1Top && !c2Top) { return -1; - } else { - // 只有c2置顶, c2排在前面 + } + + // c1 top = false && c2 top = true + if (!c1Top && c2Top) { return 1; } + // c1 top = c2 top = true || c1 top = c2 top = false + var priorityComparator = Comparator.comparing( + comment -> defaultIfNull(comment.getSpec().getPriority(), 0)); + + var creationTimeComparator = Comparator.comparing( + comment -> comment.getSpec().getCreationTime(), + Comparators.nullsLow(Comparator.reverseOrder())); + + var nameComparator = Comparator.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) { - return ObjectUtils.defaultIfNull(page, 1); + return defaultIfNull(page, 1); } int sizeNullSafe(Integer size) { - return ObjectUtils.defaultIfNull(size, DEFAULT_SIZE); + return defaultIfNull(size, DEFAULT_SIZE); } } diff --git a/application/src/test/java/run/halo/app/config/WebFluxConfigTest.java b/application/src/test/java/run/halo/app/config/WebFluxConfigTest.java index 8c0706f64..8e433318b 100644 --- a/application/src/test/java/run/halo/app/config/WebFluxConfigTest.java +++ b/application/src/test/java/run/halo/app/config/WebFluxConfigTest.java @@ -1,6 +1,7 @@ package run.halo.app.config; import java.util.List; +import org.hamcrest.core.StringStartsWith; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -30,7 +31,8 @@ class WebFluxConfigTest { .forEach(uri -> webClient.get().uri(uri) .exchange() .expectStatus().isOk() - .expectBody(String.class).isEqualTo("console index\n")); + .expectBody(String.class).value(StringStartsWith.startsWith("console index")) + ); } @Test @@ -38,7 +40,7 @@ class WebFluxConfigTest { webClient.get().uri("/console/assets/fake.txt") .exchange() .expectStatus().isOk() - .expectBody(String.class).isEqualTo("fake.\n"); + .expectBody(String.class).value(StringStartsWith.startsWith("fake.")); } @Test diff --git a/application/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java b/application/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java index c39792a7c..8846b2f76 100644 --- a/application/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java +++ b/application/src/test/java/run/halo/app/core/extension/endpoint/PostEndpointTest.java @@ -50,6 +50,7 @@ class PostEndpointTest { @BeforeEach void setUp() { + postEndpoint.setMaxAttemptsWaitForPublish(3); webTestClient = WebTestClient .bindToRouterFunction(postEndpoint.endpoint()) .build(); @@ -170,7 +171,7 @@ class PostEndpointTest { .is5xxServerError(); // 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)); } diff --git a/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java b/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java index a2e46134e..254a0ea05 100644 --- a/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java +++ b/application/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java @@ -3,7 +3,9 @@ package run.halo.app.core.extension.reconciler; import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; 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.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -369,12 +371,15 @@ class PluginReconcilerTest { @Test void resolvePluginPathAnnotation() { - when(haloPluginManager.getPluginsRoot()).thenReturn(Paths.get("/tmp/plugins")); - String path = pluginReconciler.resolvePluginPathForAnno("/tmp/plugins/sitemap-1.0.jar"); + var pluginRoot = Paths.get("tmp", "plugins"); + when(haloPluginManager.getPluginsRoot()).thenReturn(pluginRoot); + var path = pluginReconciler.resolvePluginPathForAnno( + pluginRoot.resolve("sitemap-1.0.jar").toString()); assertThat(path).isEqualTo("sitemap-1.0.jar"); - path = pluginReconciler.resolvePluginPathForAnno("/abc/plugins/sitemap-1.0.jar"); - assertThat(path).isEqualTo("/abc/plugins/sitemap-1.0.jar"); + var givenPath = Paths.get("abc", "plugins", "sitemap-1.0.jar"); + path = pluginReconciler.resolvePluginPathForAnno(givenPath.toString()); + assertThat(path).isEqualTo(givenPath.toString()); } @Nested @@ -457,17 +462,18 @@ class PluginReconcilerTest { assertThat(pluginReconciler.toPath("")).isNull(); assertThat(pluginReconciler.toPath(" ")).isNull(); - Path path = pluginReconciler.toPath("file:///path/to/file.txt"); - assertThat(path).isNotNull(); - assertThat(path.toString()).isEqualTo("/path/to/file.txt"); + final var filePath = Paths.get("path", "to", "file.txt").toAbsolutePath(); - assertThat(pluginReconciler.toPath("C:\\Users\\faker\\halo\\plugins").toString()) - .isEqualTo("C:\\Users\\faker\\halo\\plugins"); - assertThat(pluginReconciler.toPath("C:/Users/faker/halo/plugins").toString()) - .isEqualTo("C:/Users/faker/halo/plugins"); - Path windowsPath = Paths.get("C:/Users/username/Documents/file.txt"); - assertThat(pluginReconciler.toPath("file://C:/Users/username/Documents/file.txt")) - .isEqualTo(windowsPath); + // test for file:/// + assertEquals(filePath, pluginReconciler.toPath(filePath.toUri().toString())); + // test for absolute path /home/xyz or C:\Windows + assertEquals(filePath, pluginReconciler.toPath(filePath.toString())); + + var exception = assertThrows(IllegalArgumentException.class, () -> { + var fileUri = filePath.toUri(); + pluginReconciler.toPath(fileUri.toString().replaceFirst("file", "http")); + }); + assertTrue(exception.getMessage().contains("not reside in the file system")); } @Test @@ -483,8 +489,9 @@ class PluginReconcilerTest { }); // Test with non-empty pathString - URI uri = pluginReconciler.toUri("/path/to/file"); - Assertions.assertEquals("file:///path/to/file", uri.toString()); + var filePath = Paths.get("path", "to", "file"); + URI uri = pluginReconciler.toUri(filePath.toString()); + assertEquals(filePath.toUri(), uri); } } diff --git a/application/src/test/java/run/halo/app/core/extension/service/impl/PluginServiceImplTest.java b/application/src/test/java/run/halo/app/core/extension/service/impl/PluginServiceImplTest.java index ea444c636..4cd510e2e 100644 --- a/application/src/test/java/run/halo/app/core/extension/service/impl/PluginServiceImplTest.java +++ b/application/src/test/java/run/halo/app/core/extension/service/impl/PluginServiceImplTest.java @@ -207,8 +207,9 @@ class PluginServiceImplTest { String pluginName = "test-plugin"; PluginWrapper pluginWrapper = mock(PluginWrapper.class); when(pluginManager.getPlugin(pluginName)).thenReturn(pluginWrapper); + var pluginPath = Paths.get("tmp", "plugins", "fake-plugin.jar"); when(pluginWrapper.getPluginPath()) - .thenReturn(Paths.get("/tmp/plugins/fake-plugin.jar")); + .thenReturn(pluginPath); Plugin plugin = new Plugin(); plugin.setMetadata(new Metadata()); plugin.getMetadata().setName(pluginName); @@ -224,7 +225,7 @@ class PluginServiceImplTest { verify(client, times(1)).update( argThat(p -> { String reloadPath = p.getMetadata().getAnnotations().get(PluginConst.RELOAD_ANNO); - assertThat(reloadPath).isEqualTo("/tmp/plugins/fake-plugin.jar"); + assertThat(reloadPath).isEqualTo(pluginPath.toString()); return true; }) ); diff --git a/application/src/test/java/run/halo/app/infra/utils/FileUtilsTest.java b/application/src/test/java/run/halo/app/infra/utils/FileUtilsTest.java index e4ba7195f..3365edc11 100644 --- a/application/src/test/java/run/halo/app/infra/utils/FileUtilsTest.java +++ b/application/src/test/java/run/halo/app/infra/utils/FileUtilsTest.java @@ -64,8 +64,9 @@ class FileUtilsTest { unzip(zis, unzipTarget); } - var content = Files.readString(unzipTarget.resolve("examplefile")); - assertEquals("Here is an example file.\n", content); + var lines = Files.readAllLines(unzipTarget.resolve("examplefile")); + assertEquals(1, lines.size()); + assertEquals("Here is an example file.", lines.get(0)); } @Test @@ -79,9 +80,9 @@ class FileUtilsTest { try (var zis = new ZipInputStream(Files.newInputStream(zipPath))) { unzip(zis, unzipTarget); } - - var content = Files.readString(unzipTarget.resolve("examplefile")); - assertEquals("Here is an example file.\n", content); + var lines = Files.readAllLines(unzipTarget.resolve("examplefile")); + assertEquals(1, lines.size()); + assertEquals("Here is an example file.", lines.get(0)); } @Test diff --git a/application/src/test/java/run/halo/app/theme/ThemeConfigurationTest.java b/application/src/test/java/run/halo/app/theme/ThemeConfigurationTest.java index 8df630d57..500180552 100644 --- a/application/src/test/java/run/halo/app/theme/ThemeConfigurationTest.java +++ b/application/src/test/java/run/halo/app/theme/ThemeConfigurationTest.java @@ -23,7 +23,7 @@ class ThemeConfigurationTest { @InjectMocks private ThemeConfiguration themeConfiguration; - private final Path themeRoot = Paths.get("/tmp/.halo/themes"); + private final Path themeRoot = Paths.get("tmp", ".halo", "themes"); @BeforeEach void setUp() { @@ -33,25 +33,28 @@ class ThemeConfigurationTest { @Test void themeAssets() { 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"); - 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(() -> { - themeConfiguration.getThemeAssetsPath("fake-theme", "../../hello.jpg"); - }).isInstanceOf(AccessDeniedException.class) - .hasMessage( - "403 FORBIDDEN \"Directory traversal detected: /tmp/" - + ".halo/themes/fake-theme/templates/assets/../../hello.jpg\""); + assertThatThrownBy(() -> + themeConfiguration.getThemeAssetsPath("fake-theme", "../../hello.jpg")) + .isInstanceOf(AccessDeniedException.class) + .hasMessageContaining("Directory traversal detected"); 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"); - 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"); - 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"))); } } diff --git a/application/src/test/java/run/halo/app/theme/dialect/GeneratorMetaProcessorTest.java b/application/src/test/java/run/halo/app/theme/dialect/GeneratorMetaProcessorTest.java new file mode 100644 index 000000000..a732d7aec --- /dev/null +++ b/application/src/test/java/run/halo/app/theme/dialect/GeneratorMetaProcessorTest.java @@ -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(); + } + +} diff --git a/application/src/test/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImplTest.java b/application/src/test/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImplTest.java index 04b2d42a2..c4a3eaf20 100644 --- a/application/src/test/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImplTest.java +++ b/application/src/test/java/run/halo/app/theme/finders/impl/CommentPublicQueryServiceImplTest.java @@ -164,7 +164,7 @@ class CommentPublicQueryServiceImplTest { .map(Comment::getMetadata) .map(MetadataOperator::getName) .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 diff --git a/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java b/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java index dc63f6b44..6948c4e96 100644 --- a/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java +++ b/application/src/test/java/run/halo/app/theme/message/ThemeMessageResolverIntegrationTest.java @@ -68,21 +68,9 @@ public class ThemeMessageResolverIntegrationTest { .exchange() .expectStatus() .isOk() - .expectBody(String.class) - .isEqualTo(""" - - - - - Title - - - index -
zh
-
欢迎来到首页
- - - """); + .expectBody() + .xpath("/html/body/div[1]").isEqualTo("zh") + .xpath("/html/body/div[2]").isEqualTo("欢迎来到首页"); } @Test @@ -92,21 +80,9 @@ public class ThemeMessageResolverIntegrationTest { .exchange() .expectStatus() .isOk() - .expectBody(String.class) - .isEqualTo(""" - - - - - Title - - - index -
en
-
Welcome to the index
- - - """); + .expectBody() + .xpath("/html/body/div[1]").isEqualTo("en") + .xpath("/html/body/div[2]").isEqualTo("Welcome to the index"); } @Test @@ -116,21 +92,9 @@ public class ThemeMessageResolverIntegrationTest { .exchange() .expectStatus() .isOk() - .expectBody(String.class) - .isEqualTo(""" - - - - - Title - - - index -
foo
-
欢迎来到首页
- - - """); + .expectBody() + .xpath("/html/body/div[1]").isEqualTo("foo") + .xpath("/html/body/div[2]").isEqualTo("欢迎来到首页"); } @Test @@ -140,21 +104,11 @@ public class ThemeMessageResolverIntegrationTest { .exchange() .expectStatus() .isOk() - .expectBody(String.class) - .isEqualTo(""" - - - - - Title - - - index -
zh
-
欢迎来到首页
- - - """); + .expectBody() + .xpath("/html/head/title").isEqualTo("Title") + .xpath("/html/body/div[1]").isEqualTo("zh") + .xpath("/html/body/div[2]").isEqualTo("欢迎来到首页") + ; // For other theme when(themeResolver.getTheme(any(ServerWebExchange.class))) @@ -162,35 +116,16 @@ public class ThemeMessageResolverIntegrationTest { webTestClient.get() .uri("/index?language=zh") .exchange() - .expectBody(String.class) - .isEqualTo(""" - - - - - Other theme title - - -

Other 首页

- - - """); + .expectBody() + .xpath("/html/head/title").isEqualTo("Other theme title") + .xpath("/html/body/p").isEqualTo("Other 首页"); + webTestClient.get() .uri("/index?language=en") .exchange() - .expectBody(String.class) - .isEqualTo(""" - - - - - Other theme title - - -

other index

- - - """); + .expectBody() + .xpath("/html/head/title").isEqualTo("Other theme title") + .xpath("/html/body/p").isEqualTo("other index"); } ThemeContext createDefaultContext() throws URISyntaxException { diff --git a/application/src/test/resources/themes/default/templates/index.html b/application/src/test/resources/themes/default/templates/index.html index 441ad470c..7d38411c4 100644 --- a/application/src/test/resources/themes/default/templates/index.html +++ b/application/src/test/resources/themes/default/templates/index.html @@ -1,7 +1,7 @@ - + Title diff --git a/application/src/test/resources/themes/other/templates/index.html b/application/src/test/resources/themes/other/templates/index.html index ca476f69c..2ecf09eaf 100644 --- a/application/src/test/resources/themes/other/templates/index.html +++ b/application/src/test/resources/themes/other/templates/index.html @@ -1,7 +1,7 @@ - + Other theme title diff --git a/console/console-src/composables/use-editor-extension-points.ts b/console/console-src/composables/use-editor-extension-points.ts index 0dbbff05d..a1f3ab75a 100644 --- a/console/console-src/composables/use-editor-extension-points.ts +++ b/console/console-src/composables/use-editor-extension-points.ts @@ -1,6 +1,6 @@ import { usePluginModuleStore } from "@/stores/plugin"; 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 Logo from "@/assets/logo.png"; import { useI18n } from "vue-i18n"; @@ -18,11 +18,13 @@ export function useEditorExtensionPoints(): useEditorExtensionPointsReturn { { name: "default", displayName: t("core.plugin.extension_points.editor.providers.default"), - component: defineAsyncComponent({ - loader: () => import("@/components/editor/DefaultEditor.vue"), - loadingComponent: VLoading, - delay: 200, - }), + component: markRaw( + defineAsyncComponent({ + loader: () => import("@/components/editor/DefaultEditor.vue"), + loadingComponent: VLoading, + delay: 200, + }) + ), rawType: "HTML", logo: Logo, }, diff --git a/console/console-src/setup/setupModules.ts b/console/console-src/setup/setupModules.ts index 3ef95f71e..bfab108a7 100644 --- a/console/console-src/setup/setupModules.ts +++ b/console/console-src/setup/setupModules.ts @@ -21,7 +21,7 @@ export async function setupPluginModules(app: App) { const { load } = useScriptTag( `${ 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(); @@ -45,7 +45,7 @@ export async function setupPluginModules(app: App) { await loadStyle( `${ 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) { const message = i18n.global.t("core.plugin.loader.toast.style_load_failed"); diff --git a/console/src/components/editor/DefaultEditor.vue b/console/src/components/editor/DefaultEditor.vue index 3d1bd9327..20be95be2 100644 --- a/console/src/components/editor/DefaultEditor.vue +++ b/console/src/components/editor/DefaultEditor.vue @@ -86,6 +86,7 @@ import { OverlayScrollbarsComponent } from "overlayscrollbars-vue"; import { usePluginModuleStore } from "@/stores/plugin"; import type { PluginModule } from "@halo-dev/console-shared"; import { useDebounceFn } from "@vueuse/core"; +import { onBeforeUnmount } from "vue"; const { t } = useI18n(); @@ -355,6 +356,10 @@ onMounted(() => { }); }); +onBeforeUnmount(() => { + editor.value?.destroy(); +}); + // image drag and paste upload const { policies } = useFetchAttachmentPolicy(); @@ -491,203 +496,205 @@ const currentLocale = i18n.global.locale.value as diff --git a/console/src/components/form/AnnotationsForm.vue b/console/src/components/form/AnnotationsForm.vue index 728b87fdc..b442ffa03 100644 --- a/console/src/components/form/AnnotationsForm.vue +++ b/console/src/components/form/AnnotationsForm.vue @@ -17,10 +17,12 @@ import { randomUUID } from "@/utils/id"; const themeStore = useThemeStore(); function keyValidationRule(node: FormKitNode) { - return ( - !annotations.value?.[node.value as string] && - !customAnnotationsDuplicateKey.value - ); + const validAnnotations = [ + ...Object.keys(annotations.value), + ...customAnnotationsState.value.map((item) => item.key), + ]; + const count = validAnnotations.filter((item) => item === node.value); + return count.length < 2; } const props = withDefaults( @@ -73,12 +75,6 @@ const annotations = ref<{ }>({}); 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(() => { return customAnnotationsState.value.reduce((acc, cur) => { acc[cur.key] = cur.value; diff --git a/console/src/components/preview/UrlPreviewModal.vue b/console/src/components/preview/UrlPreviewModal.vue index 08fd413eb..2ae8a9792 100644 --- a/console/src/components/preview/UrlPreviewModal.vue +++ b/console/src/components/preview/UrlPreviewModal.vue @@ -1,5 +1,13 @@