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 aceb37e16..5bc4e3495 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 @@ -10,6 +10,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -17,6 +18,9 @@ import java.util.Objects; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.pf4j.DependencyResolver; +import org.pf4j.PluginDescriptor; +import org.pf4j.PluginManager; import org.pf4j.PluginWrapper; import org.pf4j.RuntimeMode; import org.springframework.core.io.Resource; @@ -37,14 +41,15 @@ import run.halo.app.core.extension.service.PluginService; import run.halo.app.extension.ReactiveExtensionClient; import run.halo.app.infra.SystemVersionSupplier; import run.halo.app.infra.exception.PluginAlreadyExistsException; +import run.halo.app.infra.exception.PluginDependencyException; import run.halo.app.infra.exception.PluginInstallationException; import run.halo.app.infra.exception.UnsatisfiedAttributeValueException; import run.halo.app.infra.utils.FileUtils; import run.halo.app.infra.utils.VersionUtils; -import run.halo.app.plugin.HaloPluginManager; import run.halo.app.plugin.PluginConst; import run.halo.app.plugin.PluginProperties; import run.halo.app.plugin.PluginUtils; +import run.halo.app.plugin.YamlPluginDescriptorFinder; import run.halo.app.plugin.YamlPluginFinder; import run.halo.app.plugin.resources.BundleResourceUtils; @@ -62,7 +67,7 @@ public class PluginServiceImpl implements PluginService { private final PluginProperties pluginProperties; - private final HaloPluginManager pluginManager; + private final PluginManager pluginManager; @Override public Flux getPresets() { @@ -82,11 +87,13 @@ public class PluginServiceImpl implements PluginService { @Override public Mono install(Path path) { return findPluginManifest(path) - .flatMap(pluginInPath -> { + .doOnNext(plugin -> { // validate the plugin version - satisfiesRequiresVersion(pluginInPath); - - return client.fetch(Plugin.class, pluginInPath.getMetadata().getName()) + satisfiesRequiresVersion(plugin); + checkDependencies(plugin); + }) + .flatMap(pluginInPath -> + client.fetch(Plugin.class, pluginInPath.getMetadata().getName()) .flatMap(oldPlugin -> Mono.error( new PluginAlreadyExistsException(oldPlugin.getMetadata().getName()))) .switchIfEmpty(Mono.defer( @@ -97,18 +104,47 @@ public class PluginServiceImpl implements PluginService { p.getSpec().setEnabled(false); }) .flatMap(client::create)) - ); - }); + )); + } + + private void checkDependencies(Plugin plugin) { + var resolvedPlugins = new ArrayList<>(pluginManager.getResolvedPlugins()); + var pluginDescriptors = new ArrayList(resolvedPlugins.size() + 1); + + resolvedPlugins.stream() + .map(PluginWrapper::getDescriptor) + .forEach(pluginDescriptors::add); + + var pluginDescriptor = YamlPluginDescriptorFinder.convert(plugin); + pluginDescriptors.add(pluginDescriptor); + + var deptResolver = new DependencyResolver(pluginManager.getVersionManager()); + var result = deptResolver.resolve(pluginDescriptors); + if (result.hasCyclicDependency()) { + throw new PluginDependencyException.CyclicException(); + } + var notFoundDependencies = result.getNotFoundDependencies(); + if (!CollectionUtils.isEmpty(notFoundDependencies)) { + throw new PluginDependencyException.NotFoundException(notFoundDependencies); + } + + var wrongVersionDependencies = result.getWrongVersionDependencies(); + if (!CollectionUtils.isEmpty(wrongVersionDependencies)) { + throw new PluginDependencyException.WrongVersionsException(wrongVersionDependencies); + } } @Override public Mono upgrade(String name, Path path) { return findPluginManifest(path) + .doOnNext(plugin -> { + satisfiesRequiresVersion(plugin); + checkDependencies(plugin); + }) .flatMap(pluginInPath -> { // pre-check the plugin in the path Assert.notNull(pluginInPath.statusNonNull().getLoadLocation(), "plugin.status.load-location must not be null"); - satisfiesRequiresVersion(pluginInPath); if (!Objects.equals(name, pluginInPath.getMetadata().getName())) { return Mono.error(new ServerWebInputException( "The provided plugin " + pluginInPath.getMetadata().getName() diff --git a/application/src/main/java/run/halo/app/infra/exception/PluginDependencyException.java b/application/src/main/java/run/halo/app/infra/exception/PluginDependencyException.java new file mode 100644 index 000000000..04cc2f344 --- /dev/null +++ b/application/src/main/java/run/halo/app/infra/exception/PluginDependencyException.java @@ -0,0 +1,56 @@ +package run.halo.app.infra.exception; + +import java.net.URI; +import java.util.List; +import org.pf4j.DependencyResolver.WrongDependencyVersion; +import org.springframework.web.server.ServerWebInputException; + +public abstract class PluginDependencyException extends ServerWebInputException { + + public PluginDependencyException(String reason) { + super(reason); + } + + public PluginDependencyException(String reason, Throwable cause) { + super(reason, null, cause); + } + + protected PluginDependencyException(String reason, Throwable cause, + String messageDetailCode, Object[] messageDetailArguments) { + super(reason, null, cause, messageDetailCode, messageDetailArguments); + } + + public static class CyclicException extends PluginDependencyException { + + public static final String TYPE = "https://halo.run/probs/plugin-cyclic-dependency"; + + public CyclicException() { + super("A cyclic dependency was detected."); + setType(URI.create(TYPE)); + } + } + + public static class NotFoundException extends PluginDependencyException { + + public static final String TYPE = "https://halo.run/probs/plugin-dependencies-not-found"; + + public NotFoundException(List dependencies) { + super("Dependencies were not found.", null, null, new Object[] {dependencies}); + setType(URI.create(TYPE)); + getBody().setProperty("dependencies", dependencies); + } + + } + + public static class WrongVersionsException extends PluginDependencyException { + + public static final String TYPE = + "https://halo.run/probs/plugin-dependencies-with-wrong-versions"; + + public WrongVersionsException(List versions) { + super("Dependencies have wrong version.", null, null, new Object[] {versions}); + setType(URI.create(TYPE)); + getBody().setProperty("versions", versions); + } + } +} diff --git a/application/src/main/java/run/halo/app/plugin/YamlPluginDescriptorFinder.java b/application/src/main/java/run/halo/app/plugin/YamlPluginDescriptorFinder.java index 4ba7d6b44..19c76d5bc 100644 --- a/application/src/main/java/run/halo/app/plugin/YamlPluginDescriptorFinder.java +++ b/application/src/main/java/run/halo/app/plugin/YamlPluginDescriptorFinder.java @@ -43,7 +43,7 @@ public class YamlPluginDescriptorFinder implements PluginDescriptorFinder { return convert(plugin); } - private DefaultPluginDescriptor convert(Plugin plugin) { + public static PluginDescriptor convert(Plugin plugin) { String pluginId = plugin.getMetadata().getName(); Plugin.PluginSpec spec = plugin.getSpec(); Plugin.PluginAuthor author = spec.getAuthor(); @@ -66,7 +66,7 @@ public class YamlPluginDescriptorFinder implements PluginDescriptorFinder { return defaultPluginDescriptor; } - private String joinLicense(List licenses) { + private static String joinLicense(List licenses) { if (CollectionUtils.isEmpty(licenses)) { return StringUtils.EMPTY; } diff --git a/application/src/main/resources/config/i18n/messages.properties b/application/src/main/resources/config/i18n/messages.properties index 24afe5e82..3be011e08 100644 --- a/application/src/main/resources/config/i18n/messages.properties +++ b/application/src/main/resources/config/i18n/messages.properties @@ -21,6 +21,9 @@ problemDetail.title.run.halo.app.infra.exception.DuplicateNameException=Duplicat problemDetail.title.run.halo.app.infra.exception.RateLimitExceededException=Request Not Permitted problemDetail.title.run.halo.app.infra.exception.NotFoundException=Resource Not Found problemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=Email Verification Failed +problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=Cyclic Dependency Detected +problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies Not Found +problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Wrong Dependency Version problemDetail.title.internalServerError=Internal Server Error # Detail definitions @@ -40,6 +43,10 @@ problemDetail.run.halo.app.infra.exception.DuplicateNameException=Duplicate name problemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=Plugin {0} already exists. problemDetail.run.halo.app.infra.exception.RateLimitExceededException=API rate limit exceeded, please try again later. problemDetail.run.halo.app.infra.exception.EmailVerificationFailed=Invalid email verification code. +problemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=A cyclic dependency was detected. +problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=Dependencies "{0}" were not found. +problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=Dependencies have wrong version: {0}. + problemDetail.index.duplicateKey=The value of {0} already exists for unique index {1}, please rename it and retry. problemDetail.user.email.verify.maxAttempts=Too many verification attempts, please try again later. problemDetail.user.password.unsatisfied=The password does not meet the specifications. diff --git a/application/src/main/resources/config/i18n/messages_zh.properties b/application/src/main/resources/config/i18n/messages_zh.properties index 956c7eaa4..7ae8650cc 100644 --- a/application/src/main/resources/config/i18n/messages_zh.properties +++ b/application/src/main/resources/config/i18n/messages_zh.properties @@ -9,6 +9,9 @@ problemDetail.title.run.halo.app.infra.exception.ThemeInstallationException=主 problemDetail.title.run.halo.app.infra.exception.RateLimitExceededException=请求限制 problemDetail.title.run.halo.app.infra.exception.NotFoundException=资源不存在 problemDetail.title.run.halo.app.infra.exception.EmailVerificationFailed=邮箱验证失败 +problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$CyclicException=循环依赖 +problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖未找到 +problemDetail.title.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本错误 problemDetail.title.internalServerError=服务器内部错误 problemDetail.org.springframework.security.authentication.BadCredentialsException=用户名或密码错误。 @@ -17,6 +20,10 @@ problemDetail.run.halo.app.infra.exception.DuplicateNameException=检测到有 problemDetail.run.halo.app.infra.exception.PluginAlreadyExistsException=插件 {0} 已经存在。 problemDetail.run.halo.app.infra.exception.RateLimitExceededException=请求过于频繁,请稍候再试。 problemDetail.run.halo.app.infra.exception.EmailVerificationFailed=验证码错误或已失效。 +problemDetail.run.halo.app.infra.exception.PluginDependencyException$CyclicException=检测到循环依赖。 +problemDetail.run.halo.app.infra.exception.PluginDependencyException$NotFoundException=依赖“{0}”未找到。 +problemDetail.run.halo.app.infra.exception.PluginDependencyException$WrongVersionsException=依赖版本有误:{0}。 + problemDetail.index.duplicateKey=唯一索引 {1} 中的值 {0} 已存在,请更名后重试。 problemDetail.user.email.verify.maxAttempts=尝试次数过多,请稍候再试。 problemDetail.user.password.unsatisfied=密码不符合规范。