From bbc6f23b2df68e92af031fb6282343c0c40d08b6 Mon Sep 17 00:00:00 2001 From: John Niang Date: Mon, 1 Jul 2024 17:25:17 +0800 Subject: [PATCH 1/7] Fix the problem of null type of search result (#6241) #### What type of PR is this? /kind bug /area core /milestone 2.17.0 #### What this PR does / why we need it: This PR adds missed type to HaloDocument while converting. #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/6235 #### Does this PR introduce a user-facing change? ```release-note None ``` --- .../main/java/run/halo/app/search/lucene/LuceneSearchEngine.java | 1 + .../app/search/lucene/LuceneSearchEngineIntegrationTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java b/application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java index 294e7a1aa5..73559980f4 100644 --- a/application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java +++ b/application/src/main/java/run/halo/app/search/lucene/LuceneSearchEngine.java @@ -348,6 +348,7 @@ private static class DocumentConverter implements Converterhalo post", doc.getTitle()); assertNull(doc.getDescription()); assertEquals("halo", doc.getContent()); From 1f4bf8ea472501d350fad1b40b02831d1e3e503e Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:45:17 +0800 Subject: [PATCH 2/7] feat: enhance PluginFinder to support check plugin availability by version (#6236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /area theme /milestone 2.17.x #### What this PR does / why we need it: 支持在主题中检查已启动的插件是否符合指定的版本要求,以便可以在某些功能可以正常工作时才渲染 示例 ```html

pluginFinder.available("fake-plugin", null)) + .isInstanceOf(IllegalArgumentException.class); + + boolean available = pluginFinder.available("fake-plugin", "1.0.0"); + assertThat(available).isFalse(); + + PluginWrapper mockPluginWrapper = Mockito.mock(PluginWrapper.class); + when(haloPluginManager.getPlugin(eq("fake-plugin"))) + .thenReturn(mockPluginWrapper); + + when(mockPluginWrapper.getPluginState()).thenReturn(PluginState.STARTED); + var descriptor = mock(PluginDescriptor.class); + when(mockPluginWrapper.getDescriptor()).thenReturn(descriptor); + when(descriptor.getVersion()).thenReturn("1.0.0"); + + available = pluginFinder.available("fake-plugin", "1.0.0"); + assertThat(available).isTrue(); + + available = pluginFinder.available("fake-plugin", ">=1.0.0"); + assertThat(available).isTrue(); + + available = pluginFinder.available("fake-plugin", "<2.0.0"); + assertThat(available).isTrue(); + + available = pluginFinder.available("fake-plugin", "2.0.0"); + assertThat(available).isFalse(); + + available = pluginFinder.available("fake-plugin", "<1.0.0"); + assertThat(available).isFalse(); + } } \ No newline at end of file From f5ebd9fe43403420a4407cac92b944cedd67b4e5 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Mon, 1 Jul 2024 17:49:17 +0800 Subject: [PATCH 3/7] feat: add TemplateFooterProcessor extension point for extending footer tag content in theme (#6191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature /area core /milestone 2.17.x #### What this PR does / why we need it: 提供对模板中 halo footer 标签内容的扩展点以支持扩展页脚内容 #### Which issue(s) this PR fixes: Fixes #6189 #### Does this PR introduce a user-facing change? ```release-note 提供对模板中 halo footer 标签内容的扩展点以支持扩展页脚内容 ``` --- .../dialect/TemplateFooterProcessor.java | 20 +++ .../TemplateFooterElementTagProcessor.java | 27 +++- .../extensionpoint-definitions.yaml | 12 ++ .../dialect/HaloProcessorDialectTest.java | 6 + ...TemplateFooterElementTagProcessorTest.java | 136 ++++++++++++++++++ 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 api/src/main/java/run/halo/app/theme/dialect/TemplateFooterProcessor.java create mode 100644 application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java diff --git a/api/src/main/java/run/halo/app/theme/dialect/TemplateFooterProcessor.java b/api/src/main/java/run/halo/app/theme/dialect/TemplateFooterProcessor.java new file mode 100644 index 0000000000..78f32158bb --- /dev/null +++ b/api/src/main/java/run/halo/app/theme/dialect/TemplateFooterProcessor.java @@ -0,0 +1,20 @@ +package run.halo.app.theme.dialect; + +import org.pf4j.ExtensionPoint; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.model.IModel; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import reactor.core.publisher.Mono; + +/** + * Theme template footer tag snippet injection processor. + * + * @author guqing + * @since 2.17.0 + */ +public interface TemplateFooterProcessor extends ExtensionPoint { + + Mono process(ITemplateContext context, IProcessableElementTag tag, + IElementTagStructureHandler structureHandler, IModel model); +} diff --git a/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java b/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java index a126f03b20..74239b1f2e 100644 --- a/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java +++ b/application/src/main/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessor.java @@ -1,14 +1,19 @@ package run.halo.app.theme.dialect; +import static org.thymeleaf.spring6.context.SpringContextUtils.getApplicationContext; + import org.springframework.context.ApplicationContext; import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.model.IModel; import org.thymeleaf.model.IProcessableElementTag; import org.thymeleaf.processor.element.AbstractElementTagProcessor; import org.thymeleaf.processor.element.IElementTagStructureHandler; import org.thymeleaf.spring6.context.SpringContextUtils; import org.thymeleaf.templatemode.TemplateMode; +import reactor.core.publisher.Flux; import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; import run.halo.app.infra.SystemSetting; +import run.halo.app.plugin.extensionpoint.ExtensionGetter; /** *

Footer element tag processor.

@@ -42,12 +47,23 @@ public TemplateFooterElementTagProcessor(final String dialectPrefix) { @Override protected void doProcess(ITemplateContext context, IProcessableElementTag tag, IElementTagStructureHandler structureHandler) { + + IModel modelToInsert = context.getModelFactory().createModel(); /* * Obtain the Spring application context. */ final ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context); + String globalFooterText = getGlobalFooterText(appCtx); - structureHandler.replaceWith(globalFooterText, false); + modelToInsert.add(context.getModelFactory().createText(globalFooterText)); + + getTemplateFooterProcessors(context) + .concatMap(processor -> processor.process(context, tag, + structureHandler, modelToInsert) + ) + .then() + .block(); + structureHandler.replaceWith(modelToInsert, false); } private String getGlobalFooterText(ApplicationContext appCtx) { @@ -57,4 +73,13 @@ private String getGlobalFooterText(ApplicationContext appCtx) { .map(SystemSetting.CodeInjection::getFooter) .block(); } + + private Flux getTemplateFooterProcessors(ITemplateContext context) { + var extensionGetter = getApplicationContext(context).getBeanProvider(ExtensionGetter.class) + .getIfUnique(); + if (extensionGetter == null) { + return Flux.empty(); + } + return extensionGetter.getExtensions(TemplateFooterProcessor.class); + } } diff --git a/application/src/main/resources/extensions/extensionpoint-definitions.yaml b/application/src/main/resources/extensions/extensionpoint-definitions.yaml index 98583bac8c..1a042a3a13 100644 --- a/application/src/main/resources/extensions/extensionpoint-definitions.yaml +++ b/application/src/main/resources/extensions/extensionpoint-definitions.yaml @@ -76,3 +76,15 @@ spec: displayName: "搜索引擎" type: SINGLETON description: "扩展内容搜索引擎" + +--- +apiVersion: plugin.halo.run/v1alpha1 +kind: ExtensionPointDefinition +metadata: + name: template-footer-processor +spec: + className: run.halo.app.theme.dialect.TemplateFooterProcessor + displayName: 页脚标签内容处理器 + type: MULTI_INSTANCE + description: "提供用于扩展 标签内容的扩展方式。" + \ No newline at end of file diff --git a/application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java b/application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java index d0a36706bd..c4c18afe50 100644 --- a/application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java +++ b/application/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java @@ -125,6 +125,9 @@ void globalHeadAndFooterProcessors() { when(fetcher.fetch(eq(SystemSetting.Basic.GROUP), eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic)); + when(extensionGetter.getExtensions(TemplateFooterProcessor.class)) + .thenReturn(Flux.empty()); + Context context = getContext(); String result = templateEngine.process("index", context); @@ -172,6 +175,9 @@ void contentHeadAndFooterAndPostProcessors() { when(fetcher.fetch(eq(SystemSetting.Basic.GROUP), eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic)); + when(extensionGetter.getExtensions(TemplateFooterProcessor.class)) + .thenReturn(Flux.empty()); + String result = templateEngine.process("post", context); assertThat(result).isEqualTo(""" diff --git a/application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java b/application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java new file mode 100644 index 0000000000..9fec46b91e --- /dev/null +++ b/application/src/test/java/run/halo/app/theme/dialect/TemplateFooterElementTagProcessorTest.java @@ -0,0 +1,136 @@ +package run.halo.app.theme.dialect; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.ApplicationContext; +import org.thymeleaf.IEngineConfiguration; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.model.IModel; +import org.thymeleaf.model.IProcessableElementTag; +import org.thymeleaf.processor.IProcessor; +import org.thymeleaf.processor.element.IElementTagStructureHandler; +import org.thymeleaf.spring6.dialect.SpringStandardDialect; +import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext; +import org.thymeleaf.templateresolver.StringTemplateResolver; +import org.thymeleaf.templateresource.ITemplateResource; +import org.thymeleaf.templateresource.StringTemplateResource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting; +import run.halo.app.plugin.extensionpoint.ExtensionGetter; + +/** + * Tests for {@link TemplateFooterElementTagProcessor}. + * + * @author guqing + * @since 2.17.0 + */ +@ExtendWith(MockitoExtension.class) +class TemplateFooterElementTagProcessorTest { + @Mock + private ApplicationContext applicationContext; + + @Mock + ExtensionGetter extensionGetter; + + @Mock + private SystemConfigurableEnvironmentFetcher fetcher; + + private TemplateEngine templateEngine; + + @BeforeEach + void setUp() { + HaloProcessorDialect haloProcessorDialect = new MockHaloProcessorDialect(); + templateEngine = new TemplateEngine(); + templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect())); + templateEngine.addTemplateResolver(new MockTemplateResolver()); + + SystemSetting.CodeInjection codeInjection = new SystemSetting.CodeInjection(); + codeInjection.setFooter( + "

Powered by Halo

"); + lenient().when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP), + eq(SystemSetting.CodeInjection.class))).thenReturn(Mono.just(codeInjection)); + + lenient().when(applicationContext.getBeanProvider(ExtensionGetter.class)) + .thenAnswer(invocation -> { + var objectProvider = mock(ObjectProvider.class); + when(objectProvider.getIfUnique()).thenReturn(extensionGetter); + return objectProvider; + }); + lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) + .thenReturn(fetcher); + } + + @Test + void footerProcessorTest() { + when(extensionGetter.getExtensions(TemplateFooterProcessor.class)) + .thenReturn(Flux.just(new FakeFooterCodeInjection())); + + String result = templateEngine.process("fake-template", getContext()); + // footer injected code is not processable + assertThat(result).isEqualToIgnoringWhitespace(""" +

Powered by Halo

+
© 2024 guqing's blog
+
+ """); + } + + private Context getContext() { + Context context = new Context(); + context.setVariable( + ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME, + new ThymeleafEvaluationContext(applicationContext, null)); + return context; + } + + static class MockTemplateResolver extends StringTemplateResolver { + @Override + protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration, + String ownerTemplate, String template, + Map templateResolutionAttributes) { + return new StringTemplateResource(""" + + """); + } + } + + static class MockHaloProcessorDialect extends HaloProcessorDialect { + @Override + public Set getProcessors(String dialectPrefix) { + var processors = new HashSet(); + processors.add(new TemplateFooterElementTagProcessor(dialectPrefix)); + return processors; + } + } + + static class FakeFooterCodeInjection implements TemplateFooterProcessor { + + @Override + public Mono process(ITemplateContext context, IProcessableElementTag tag, + IElementTagStructureHandler structureHandler, IModel model) { + var factory = context.getModelFactory(); + // regular footer text + var copyRight = factory.createText("
© 2024 guqing's blog
"); + model.add(copyRight); + // variable footer text + model.add(factory.createText("
")); + return Mono.empty(); + } + } +} \ No newline at end of file From 0b7b74e8268a8edd378caccb9aae4781e1cc289c Mon Sep 17 00:00:00 2001 From: Ryan Wang Date: Mon, 1 Jul 2024 17:55:17 +0800 Subject: [PATCH 4/7] fix: resolve styling issue for checkbox when disabled (#6240) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /area ui /kind bug /milestone 2.17.x #### What this PR does / why we need it: 修复 FormKit 中当 checkbox 为 disabled 时的样式问题。 before: image after: image #### Does this PR introduce a user-facing change? ```release-note None ``` --- ui/src/formkit/theme.ts | 2 +- ui/src/styles/tailwind.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/formkit/theme.ts b/ui/src/formkit/theme.ts index 52dfba5de5..499ae0231e 100644 --- a/ui/src/formkit/theme.ts +++ b/ui/src/formkit/theme.ts @@ -29,7 +29,7 @@ const theme: Record> = { global: { form: "divide-y divide-gray-100", outer: - "formkit-disabled:opacity-50 py-4 first:pt-0 last:pb-0 transition-all", + "formkit-disabled:opacity-70 formkit-disabled:cursor-not-allowed formkit-disabled:pointer-events-none py-4 first:pt-0 last:pb-0 transition-all", help: "text-xs mt-2 text-gray-500", messages: "list-none p-0 mt-1.5 mb-0 transition-all", message: "text-red-500 mt-2 text-xs", diff --git a/ui/src/styles/tailwind.css b/ui/src/styles/tailwind.css index 360a0cec8e..46cdac3bff 100644 --- a/ui/src/styles/tailwind.css +++ b/ui/src/styles/tailwind.css @@ -22,5 +22,5 @@ textarea { } input[type="checkbox"] { - @apply rounded-sm border-gray-500 disabled:bg-gray-200 disabled:opacity-50; + @apply rounded-sm border-gray-500 disabled:cursor-not-allowed disabled:border-gray-400 disabled:opacity-50 disabled:checked:border-none; } From cc3564bf8292c4420ccd3b432ff5426cfb77e12b Mon Sep 17 00:00:00 2001 From: John Niang Date: Mon, 1 Jul 2024 17:57:17 +0800 Subject: [PATCH 5/7] Add support to disable two-factor authentication (#6242) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.17.0 #### What this PR does / why we need it: This PR provides a configuration property to control whether two-factor authentication is disabled. e.g.: ```yaml halo: security: two-factor-auth: disabled: true | false # Default is false. ``` #### Which issue(s) this PR fixes: Fixes #5640 #### Special notes for your reviewer: 1. Enable 2FA and configure TOTP 2. Disable 2FA by configuring property above 3. Restart Halo and try to login #### Does this PR introduce a user-facing change? ```release-note 支持通过配置的方式全局禁用二步验证 ``` --- .../app/config/WebServerSecurityConfig.java | 8 ++++++-- .../infra/properties/SecurityProperties.java | 12 ++++++++++++ .../app/security/DefaultUserDetailService.java | 11 ++++++++++- .../security/DefaultUserDetailServiceTest.java | 18 +++++++++++++++++- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java index 17954539a0..06c191707d 100644 --- a/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java +++ b/application/src/main/java/run/halo/app/config/WebServerSecurityConfig.java @@ -127,8 +127,12 @@ public ReactiveIndexedSessionRepository reactiveSessionRepository( @Bean DefaultUserDetailService userDetailsService(UserService userService, - RoleService roleService) { - return new DefaultUserDetailService(userService, roleService); + RoleService roleService, + HaloProperties haloProperties) { + var userDetailService = new DefaultUserDetailService(userService, roleService); + var twoFactorAuthDisabled = haloProperties.getSecurity().getTwoFactorAuth().isDisabled(); + userDetailService.setTwoFactorAuthDisabled(twoFactorAuthDisabled); + return userDetailService; } @Bean diff --git a/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java b/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java index 19bfb8bb90..d67b49549a 100644 --- a/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java +++ b/application/src/main/java/run/halo/app/infra/properties/SecurityProperties.java @@ -16,6 +16,18 @@ public class SecurityProperties { private final RememberMeOptions rememberMe = new RememberMeOptions(); + private final TwoFactorAuthOptions twoFactorAuth = new TwoFactorAuthOptions(); + + @Data + public static class TwoFactorAuthOptions { + + /** + * Whether two-factor authentication is disabled. + */ + private boolean disabled; + + } + @Data public static class FrameOptions { diff --git a/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java b/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java index b56e2625ec..9d9a63e9bb 100644 --- a/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java +++ b/application/src/main/java/run/halo/app/security/DefaultUserDetailService.java @@ -7,6 +7,7 @@ import static run.halo.app.security.authorization.AuthorityUtils.AUTHENTICATED_ROLE_NAME; import static run.halo.app.security.authorization.AuthorityUtils.ROLE_PREFIX; +import lombok.Setter; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; @@ -31,6 +32,12 @@ public class DefaultUserDetailService private final RoleService roleService; + /** + * Indicates whether two-factor authentication is disabled. + */ + @Setter + private boolean twoFactorAuthDisabled; + public DefaultUserDetailService(UserService userService, RoleService roleService) { this.userService = userService; this.roleService = roleService; @@ -66,7 +73,9 @@ public Mono findByUsername(String username) { return setAuthorities.then(Mono.fromSupplier(() -> { var twoFactorAuthSettings = TwoFactorUtils.getTwoFactorAuthSettings(user); return new HaloUser.Builder(userBuilder.build()) - .twoFactorAuthEnabled(twoFactorAuthSettings.isAvailable()) + .twoFactorAuthEnabled( + (!twoFactorAuthDisabled) && twoFactorAuthSettings.isAvailable() + ) .totpEncryptedSecret(user.getSpec().getTotpEncryptedSecret()) .build(); })); diff --git a/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java b/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java index a6251fdfba..58aa4fea11 100644 --- a/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java +++ b/application/src/test/java/run/halo/app/security/DefaultUserDetailServiceTest.java @@ -156,11 +156,27 @@ void shouldFindHaloUserDetailsWith2faEnabledWhen2faEnabledAndTotpConfigured() { .verifyComplete(); } + @Test + void shouldFindHaloUserDetailsWith2faDisabledWhen2faDisabledGlobally() { + userDetailService.setTwoFactorAuthDisabled(true); + var fakeUser = createFakeUser(); + fakeUser.getSpec().setTwoFactorAuthEnabled(true); + fakeUser.getSpec().setTotpEncryptedSecret("fake-totp-encrypted-secret"); + when(userService.getUser("faker")).thenReturn(Mono.just(fakeUser)); + when(roleService.listRoleRefs(any())).thenReturn(Flux.empty()); + userDetailService.findByUsername("faker") + .as(StepVerifier::create) + .assertNext(userDetails -> { + assertInstanceOf(HaloUserDetails.class, userDetails); + assertFalse(((HaloUserDetails) userDetails).isTwoFactorAuthEnabled()); + }) + .verifyComplete(); + } + @Test void shouldFindUserDetailsByExistingUsernameButKindOfRoleRefIsNotRole() { var foundUser = createFakeUser(); - var roleGvk = new Role().groupVersionKind(); var roleRef = new RoleRef(); roleRef.setKind("FakeRole"); roleRef.setApiGroup("fake.halo.run"); From b9c500dc8db165c01b3dcf0bafd09bca0eb205f5 Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:05:17 +0800 Subject: [PATCH 6/7] fix: handle plugin entry file loading when cache temp directory is cleared (#6238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind bug /area plugin /milestone 2.17.x #### What this PR does / why we need it: 修复当插件入口文件的缓存目录被系统清理后会导致一直无法加载的问题 原问题复现步骤: 1. 登录后刷新页面,此时缓存目录被创建 2. 删除缓存目录后就会提示文件不存在然后导致插件入口文件一致无法加载直到重启 Halo #### Which issue(s) this PR fixes: Fixes #6226 #### Does this PR introduce a user-facing change? ```release-note 修复当插件入口文件的缓存目录被系统清理后会导致一直无法加载的问题 ``` --- .../service/impl/PluginServiceImpl.java | 25 ++++++++++++++++--- .../service/impl/PluginServiceImplTest.java | 19 ++++++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java b/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java index e891887f59..be5d5bd06e 100644 --- a/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java +++ b/application/src/main/java/run/halo/app/core/extension/service/impl/PluginServiceImpl.java @@ -549,15 +549,22 @@ Mono computeIfAbsent(String version, Publisher content) { // double check of the resource .filter(res -> isResourceMatch(res, newFilename)) .switchIfEmpty(Mono.using( - () -> tempDir.resolve(newFilename), + () -> { + if (!Files.exists(tempDir)) { + Files.createDirectories(tempDir); + } + return tempDir.resolve(newFilename); + }, path -> DataBufferUtils.write(content, path, CREATE, TRUNCATE_EXISTING) .then(Mono.fromSupplier( () -> new FileSystemResource(path) )), path -> { - // clean up old resource - cleanUp(this.resource); + if (shouldCleanUp(path)) { + // clean up old resource + cleanUp(this.resource); + } }) .subscribeOn(scheduler) .doOnNext(newResource -> this.resource = newResource) @@ -579,6 +586,18 @@ Mono computeIfAbsent(String version, Publisher content) { }); } + private boolean shouldCleanUp(Path newPath) { + if (this.resource == null || !this.resource.exists()) { + return false; + } + try { + var oldPath = this.resource.getFile().toPath(); + return !oldPath.equals(newPath); + } catch (IOException e) { + return false; + } + } + private static void cleanUp(Resource resource) { if (resource instanceof WritableResource wr && wr.isWritable() 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 8d34e55d65..6918aba6e9 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 @@ -46,6 +46,7 @@ import org.pf4j.PluginDescriptor; import org.pf4j.PluginWrapper; import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.util.FileSystemUtils; import org.springframework.web.server.ServerWebInputException; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -391,6 +392,24 @@ void shouldComputeBundleFileIfAbsent() { } }) .verifyComplete(); + + try { + FileSystemUtils.deleteRecursively(tempDir); + } catch (IOException e) { + throw new RuntimeException(e); + } + cache.computeIfAbsent("fake-version", fakeContent) + .as(StepVerifier::create) + .assertNext(resource -> { + try { + assertThat(Files.exists(tempDir)).isTrue(); + assertEquals(tempDir.resolve("different-version.js"), + resource.getFile().toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .verifyComplete(); } @Test From c5bf1924a60b26d63a594fe0efa0da787eb203f6 Mon Sep 17 00:00:00 2001 From: LonelySnowman <111493458+LonelySnowman@users.noreply.github.com> Date: Mon, 1 Jul 2024 23:49:18 +0800 Subject: [PATCH 7/7] feat: tag and category add save and continue button (#6223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind feature #### What this PR does / why we need it: 文章分类和标签 Modal 添加 ”保存并继续添加” 按钮,便于连续添加。 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/6127 #### Special notes for your reviewer: ![image](https://github.com/halo-dev/halo/assets/111493458/4debe13e-4002-48a8-827b-58cb74b4b074) #### Does this PR introduce a user-facing change? ```release-note 文章分类和标签页添加 "保存并继续添加" 按钮。 ``` --- .../components/CategoryEditingModal.vue | 42 +++++++++++++----- .../posts/tags/components/TagEditingModal.vue | 43 ++++++++++++++----- ui/src/locales/en.yaml | 1 + ui/src/locales/es.yaml | 1 + ui/src/locales/zh-CN.yaml | 1 + ui/src/locales/zh-TW.yaml | 1 + 6 files changed, 69 insertions(+), 20 deletions(-) diff --git a/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue b/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue index 84394a1fdb..7ccf8b3b4d 100644 --- a/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue +++ b/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue @@ -21,6 +21,7 @@ import { import { useQueryClient } from "@tanstack/vue-query"; import { cloneDeep } from "lodash-es"; import { useI18n } from "vue-i18n"; +import { submitForm, reset } from "@formkit/core"; const props = withDefaults( defineProps<{ @@ -65,6 +66,7 @@ const formState = ref({ const selectedParentCategory = ref(); const saving = ref(false); const modal = ref | null>(null); +const keepAddingSubmit = ref(false); const isUpdateMode = !!props.category; @@ -133,7 +135,11 @@ const handleSaveCategory = async () => { } } - modal.value?.close(); + if (keepAddingSubmit.value) { + reset("category-form"); + } else { + modal.value?.close(); + } queryClient.invalidateQueries({ queryKey: ["post-categories"] }); @@ -145,6 +151,11 @@ const handleSaveCategory = async () => { } }; +const handleSubmit = (keepAdding = false) => { + keepAddingSubmit.value = keepAdding; + submitForm("category-form"); +}; + onMounted(() => { if (props.category) { formState.value = cloneDeep(props.category); @@ -340,18 +351,29 @@ const { handleGenerateSlug } = useSlugify( diff --git a/ui/console-src/modules/contents/posts/tags/components/TagEditingModal.vue b/ui/console-src/modules/contents/posts/tags/components/TagEditingModal.vue index ec60168e54..a7b6378a31 100644 --- a/ui/console-src/modules/contents/posts/tags/components/TagEditingModal.vue +++ b/ui/console-src/modules/contents/posts/tags/components/TagEditingModal.vue @@ -26,6 +26,7 @@ import useSlugify from "@console/composables/use-slugify"; import { cloneDeep } from "lodash-es"; import { onMounted } from "vue"; import { useI18n } from "vue-i18n"; +import { submitForm, reset } from "@formkit/core"; const props = withDefaults( defineProps<{ @@ -63,6 +64,8 @@ const modal = ref | null>(null); const saving = ref(false); +const keepAddingSubmit = ref(false); + const isUpdateMode = computed(() => !!props.tag); const modalTitle = computed(() => { @@ -101,7 +104,11 @@ const handleSaveTag = async () => { }); } - modal.value?.close(); + if (keepAddingSubmit.value) { + reset("tag-form"); + } else { + modal.value?.close(); + } Toast.success(t("core.common.toast.save_success")); } catch (e) { @@ -111,6 +118,11 @@ const handleSaveTag = async () => { } }; +const handleSubmit = (keepAdding = false) => { + keepAddingSubmit.value = keepAdding; + submitForm("tag-form"); +}; + onMounted(() => { setFocus("displayNameInput"); }); @@ -250,18 +262,29 @@ const { handleGenerateSlug } = useSlugify( diff --git a/ui/src/locales/en.yaml b/ui/src/locales/en.yaml index 1757a21fea..158216d6b6 100644 --- a/ui/src/locales/en.yaml +++ b/ui/src/locales/en.yaml @@ -1729,6 +1729,7 @@ core: buttons: save: Save close: Close + save_and_continue: Save and keep adding close_and_shortcut: Close (Esc) delete: Delete setting: Setting diff --git a/ui/src/locales/es.yaml b/ui/src/locales/es.yaml index 9b59ed9651..8107439eba 100644 --- a/ui/src/locales/es.yaml +++ b/ui/src/locales/es.yaml @@ -1308,6 +1308,7 @@ core: buttons: save: Guardar close: Cerrar + save_and_continue: Guardar y seguir añadiendo close_and_shortcut: Cerrar (Esc) delete: Borrar setting: Configuración diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml index 222bde8731..bf342f3dc2 100644 --- a/ui/src/locales/zh-CN.yaml +++ b/ui/src/locales/zh-CN.yaml @@ -1644,6 +1644,7 @@ core: buttons: save: 保存 close: 关闭 + save_and_continue: 保存并继续添加 close_and_shortcut: 关闭(Esc) delete: 删除 setting: 设置 diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml index 2fac839b56..420e277178 100644 --- a/ui/src/locales/zh-TW.yaml +++ b/ui/src/locales/zh-TW.yaml @@ -1601,6 +1601,7 @@ core: buttons: save: 保存 close: 關閉 + save_and_continue: 保存並繼續添加 close_and_shortcut: 關閉(Esc) delete: 刪除 setting: 設置