diff --git a/application/src/main/java/run/halo/app/theme/service/ThemeServiceImpl.java b/application/src/main/java/run/halo/app/theme/service/ThemeServiceImpl.java index 1b6494b708..047210dd04 100644 --- a/application/src/main/java/run/halo/app/theme/service/ThemeServiceImpl.java +++ b/application/src/main/java/run/halo/app/theme/service/ThemeServiceImpl.java @@ -13,8 +13,10 @@ import java.nio.file.Path; import java.time.Duration; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Predicate; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -41,7 +43,10 @@ import run.halo.app.core.extension.AnnotationSetting; import run.halo.app.core.extension.Setting; import run.halo.app.core.extension.Theme; +import run.halo.app.core.extension.notification.NotificationTemplate; import run.halo.app.extension.ConfigMap; +import run.halo.app.extension.Extension; +import run.halo.app.extension.GroupVersionKind; import run.halo.app.extension.MetadataUtil; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.extension.Unstructured; @@ -114,7 +119,7 @@ private static Mono createThemeTempPath() { public Mono install(Publisher content) { var themeRoot = this.themeRoot.get(); return unzipThemeTo(content, themeRoot, scheduler) - .flatMap(this::persistent); + .flatMap(theme -> persistent(theme, false)); } @Override @@ -155,7 +160,7 @@ public Mono upgrade(String themeName, Publisher content) { return deleteThemeAndWaitForComplete(themeName) .then(copyTheme) - .then(this.persistent(newTheme)); + .then(this.persistent(newTheme, true)); }); }, tempDir -> deleteRecursivelyAndSilently(tempDir, scheduler) @@ -173,7 +178,7 @@ public Mono upgrade(String themeName, Publisher content) { * @return a theme custom model * @see Theme */ - public Mono persistent(Unstructured themeManifest) { + private Mono persistent(Unstructured themeManifest, boolean isUpgrade) { Assert.state(StringUtils.equals(Theme.KIND, themeManifest.getKind()), "Theme manifest kind must be Theme."); return client.create(themeManifest) @@ -204,34 +209,27 @@ public Mono persistent(Unstructured themeManifest) { return Mono.error(new IllegalStateException( "Theme must only have one config.yaml or config.yml.")); } - var spec = theme.getSpec(); return Flux.fromIterable(unstructureds) - .filter(unstructured -> { - String name = unstructured.getMetadata().getName(); - boolean isThemeSetting = unstructured.getKind().equals(Setting.KIND) - && StringUtils.equals(spec.getSettingName(), name); - - boolean isThemeConfig = unstructured.getKind().equals(ConfigMap.KIND) - && StringUtils.equals(spec.getConfigMapName(), name); - - boolean isAnnotationSetting = unstructured.getKind() - .equals(AnnotationSetting.KIND); - return isThemeSetting || isThemeConfig || isAnnotationSetting; - }) + .filter(unstructured -> ExtensionWhitelist.of(theme).isAllowed(unstructured)) .doOnNext(unstructured -> populateThemeNameLabel(unstructured, theme.getMetadata().getName())) - .flatMap(this::createOrUpdate) + .flatMap(unstructured -> { + if (isUpgrade) { + return createOrUpdate(unstructured); + } + return client.create(unstructured); + }) .then(Mono.just(theme)); }); } - Mono createOrUpdate(Unstructured unstructured) { + private Mono createOrUpdate(Unstructured unstructured) { return Mono.defer(() -> client.fetch(unstructured.groupVersionKind(), unstructured.getMetadata().getName()) .flatMap(existUnstructured -> { - existUnstructured.getMetadata() - .setVersion(unstructured.getMetadata().getVersion()); - return client.update(existUnstructured); + unstructured.getMetadata() + .setVersion(existUnstructured.getMetadata().getVersion()); + return client.update(unstructured); }) .switchIfEmpty(Mono.defer(() -> client.create(unstructured))) ) @@ -264,17 +262,12 @@ public Mono reloadTheme(String name) { }) .flatMap(client::update); })) - .flatMap(theme -> { - String settingName = theme.getSpec().getSettingName(); - return Flux.fromIterable(ThemeUtils.loadThemeResources(getThemePath(theme))) - .filter(unstructured -> (Setting.KIND.equals(unstructured.getKind()) - && unstructured.getMetadata().getName().equals(settingName)) - || AnnotationSetting.KIND.equals(unstructured.getKind()) - ) - .doOnNext(unstructured -> populateThemeNameLabel(unstructured, name)) - .flatMap(client::create) - .then(Mono.just(theme)); - }); + .flatMap(theme -> Flux.fromIterable(ThemeUtils.loadThemeResources(getThemePath(theme))) + .filter(unstructured -> ExtensionWhitelist.of(theme).isAllowed(unstructured)) + .doOnNext(unstructured -> populateThemeNameLabel(unstructured, name)) + .flatMap(this::createOrUpdate) + .then(Mono.just(theme)) + ); } private static void populateThemeNameLabel(Unstructured unstructured, String themeName) { @@ -373,4 +366,61 @@ Mono waitForThemeDeleted(String themeName) { new ServerErrorException("Wait timeout for theme deleted", null))) .then(); } + + static class ExtensionWhitelist { + private final Set rules; + + private ExtensionWhitelist(Theme theme) { + this.rules = getRules(theme); + } + + public static ExtensionWhitelist of(Theme theme) { + return new ExtensionWhitelist(theme); + } + + public boolean isAllowed(Unstructured unstructured) { + return this.rules.stream() + .anyMatch(rule -> rule.matches(unstructured)); + } + + private Set getRules(Theme theme) { + var rules = new HashSet(); + rules.add(AllowedExtension.of(AnnotationSetting.class)); + rules.add(AllowedExtension.of(NotificationTemplate.class)); + + var configMapName = theme.getSpec().getConfigMapName(); + if (StringUtils.isNotBlank(configMapName)) { + rules.add(AllowedExtension.of(ConfigMap.class, configMapName)); + } + + var settingName = theme.getSpec().getSettingName(); + if (StringUtils.isNotBlank(settingName)) { + rules.add(AllowedExtension.of(Setting.class, settingName)); + } + return rules; + } + } + + record AllowedExtension(String apiGroup, String kind, String name) { + AllowedExtension { + Assert.notNull(apiGroup, "The apiGroup must not be null"); + Assert.notNull(kind, "Kind must not be null"); + } + + public static AllowedExtension of(Class clazz) { + return of(clazz, null); + } + + public static AllowedExtension of(Class clazz, String name) { + var gvk = GroupVersionKind.fromExtension(clazz); + return new AllowedExtension(gvk.group(), gvk.kind(), name); + } + + public boolean matches(Unstructured unstructured) { + var groupVersionKind = unstructured.groupVersionKind(); + return this.apiGroup.equals(groupVersionKind.group()) + && this.kind.equals(groupVersionKind.kind()) + && (this.name == null || this.name.equals(unstructured.getMetadata().getName())); + } + } } diff --git a/application/src/test/java/run/halo/app/theme/service/ThemeServiceImplTest.java b/application/src/test/java/run/halo/app/theme/service/ThemeServiceImplTest.java index 184e38a573..e97864a31a 100644 --- a/application/src/test/java/run/halo/app/theme/service/ThemeServiceImplTest.java +++ b/application/src/test/java/run/halo/app/theme/service/ThemeServiceImplTest.java @@ -368,6 +368,8 @@ void reloadThemeWhenSettingNameNotSetBefore() throws IOException { when(client.list(eq(AnnotationSetting.class), any(), eq(null))).thenReturn(Flux.empty()); + when(client.fetch(eq(Setting.GVK), eq("fake-setting"))) + .thenReturn(Mono.empty()); themeService.reloadTheme("fake-theme") .as(StepVerifier::create) .consumeNextWith(themeUpdated -> {