diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index c3f787529..75370d0b8 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -714,4 +714,39 @@ public function __isset($key) return false; } + + public function extensionInstall(): static + { + // TODO: Implement extensionInstall() method. + } + + public function extensionUninstall(): static + { + // TODO: Implement extensionUninstall() method. + } + + public function extensionEnable(): static + { + // TODO: Implement extensionEnable() method. + } + + public function extensionDisable(): static + { + // TODO: Implement extensionDisable() method. + } + + public function extensionRollback(): static + { + // TODO: Implement extensionRollback() method. + } + + public function extensionRefresh(): static + { + // TODO: Implement extensionRefresh() method. + } + + public function extensionUpdate(): static + { + // TODO: Implement extensionUpdate() method. + } } diff --git a/modules/cms/classes/ThemeManager.php b/modules/cms/classes/ThemeManager.php index 95a47c369..8eb2abc1a 100644 --- a/modules/cms/classes/ThemeManager.php +++ b/modules/cms/classes/ThemeManager.php @@ -2,12 +2,13 @@ namespace Cms\Classes; -use Winter\Storm\Support\Facades\File; -use System\Classes\Extensions\WinterExtension; -use Winter\Storm\Exception\ApplicationException; +use Cms\Classes\Theme; use System\Classes\Extensions\ExtensionManager; +use System\Classes\Extensions\ExtensionSource; +use System\Classes\Extensions\WinterExtension; use System\Models\Parameter; -use Cms\Classes\Theme; +use Winter\Storm\Exception\ApplicationException; +use Winter\Storm\Support\Facades\File; /** * Theme manager @@ -33,7 +34,7 @@ public function getInstalled() * @param string $name Theme code * @return boolean */ - public function isInstalled($name) + public function isInstalled($name): bool { return array_key_exists($name, Parameter::get('system::theme.history', [])); } @@ -95,12 +96,17 @@ public function create(): Theme // TODO: Implement create() method. } - public function install(Theme|string $extension): Theme + public function install(ExtensionSource|WinterExtension|string $extension): Theme { // TODO: Implement install() method. } - public function enable(Theme|string $extension): Theme + public function getExtension(WinterExtension|ExtensionSource|string $extension): ?WinterExtension + { + // TODO: Implement getExtension() method. + } + + public function enable(WinterExtension|string $extension): Theme { // TODO: Implement enable() method. } diff --git a/modules/system/classes/extensions/ExtensionManager.php b/modules/system/classes/extensions/ExtensionManager.php index 71677ffe0..4814dc0bd 100644 --- a/modules/system/classes/extensions/ExtensionManager.php +++ b/modules/system/classes/extensions/ExtensionManager.php @@ -6,13 +6,17 @@ interface ExtensionManager { + public const EXTENSION_NAME = ''; + public function list(): array; public function create(): WinterExtension; /** * @throws ApplicationException If the installation fails */ - public function install(WinterExtension|string $extension): WinterExtension; + public function install(ExtensionSource|WinterExtension|string $extension): WinterExtension; + public function isInstalled(ExtensionSource|WinterExtension|string $extension): bool; + public function getExtension(ExtensionSource|WinterExtension|string $extension): ?WinterExtension; public function enable(WinterExtension|string $extension): mixed; public function disable(WinterExtension|string $extension): mixed; public function update(WinterExtension|string $extension): mixed; diff --git a/modules/system/classes/extensions/ExtensionSource.php b/modules/system/classes/extensions/ExtensionSource.php new file mode 100644 index 000000000..0d9ec2f7f --- /dev/null +++ b/modules/system/classes/extensions/ExtensionSource.php @@ -0,0 +1,203 @@ + PluginManager::class, + self::TYPE_THEME => ThemeManager::class, + self::TYPE_MODULE => ModuleManager::class, + ]; + + protected string $status = 'uninstalled'; + + public function __construct( + public string $source, + public string $type, + public ?string $code = null, + public ?string $composerPackage = null, + public ?string $path = null + ) { + if (!in_array($this->source, [static::SOURCE_COMPOSER, static::SOURCE_MARKET, static::SOURCE_LOCAL])) { + throw new \InvalidArgumentException("Invalid source '{$this->source}'"); + } + + if (!in_array($this->type, [static::TYPE_PLUGIN, static::TYPE_THEME, static::TYPE_MODULE])) { + throw new \InvalidArgumentException("Invalid type '{$this->type}'"); + } + + if ($this->source === static::SOURCE_COMPOSER && !$this->composerPackage) { + throw new ApplicationException('You must provide a composer package for a composer source.'); + } + + if ($this->source !== static::SOURCE_COMPOSER && !$this->code) { + if (!$this->path) { + throw new ApplicationException('You must provide a code or path.'); + } + + $this->code = $this->guessCodeFromPath($this->path); + } + + $this->status = $this->checkStatus(); + } + + public function getStatus(): string + { + return $this->status; + } + + public function getCode(): ?string + { + if ($this->code) { + return $this->code; + } + + if (!$this->path) { + return null; + } + + return $this->code = $this->guessCodeFromPath($this->path); + } + + /** + * @throws ApplicationException + */ + public function createFiles(): static + { + switch ($this->source) { + case static::SOURCE_COMPOSER: + try { + Composer::require($this->composerPackage); + } catch (CommandException $e) { + throw new ApplicationException('Unable to require composer package', previous: $e); + } + + $info = Composer::show('installed', $this->composerPackage); + $this->path = $this->relativePath($info['path']); + $this->source = static::SOURCE_LOCAL; + break; + case static::SOURCE_MARKET: + throw new ApplicationException('need to implement market support'); + break; + case static::SOURCE_LOCAL: + break; + } + + if ($this->status !== static::STATUS_INSTALLED) { + $this->status = static::STATUS_UNPACKED; + } + + return $this; + } + + /** + * @throws ApplicationException + */ + public function install(): WinterExtension + { + if ($this->status === static::STATUS_UNINSTALLED) { + throw new ApplicationException('Extension source is not unpacked'); + } + + if ($this->status === static::STATUS_INSTALLED) { + return $this->getExtensionManager()->getExtension($this); + } + + return $this->getExtensionManager()->install($this); + } + + public function uninstall(): bool + { + if ($this->status !== static::STATUS_INSTALLED) { + throw new ApplicationException('Extension source is not installed'); + } + + return $this->getExtensionManager()->uninstall($this); + } + + protected function getExtensionManager(): ExtensionManager + { + return App::make($this->extensionManagerMapping[$this->type]); + } + + protected function checkStatus(): string + { + switch ($this->source) { + case static::SOURCE_COMPOSER: + try { + $info = Composer::show('installed', $this->composerPackage); + } catch (CommandException $e) { + return static::STATUS_UNINSTALLED; + } + + $this->path = $this->relativePath($info['path']); + + if (!$this->getExtensionManager()->isInstalled($this)) { + return static::STATUS_UNPACKED; + } + break; + case static::SOURCE_MARKET: + case static::SOURCE_LOCAL: + $path = $this->path ?? $this->guessPackagePath($this->code); + if (!File::exists($path)) { + return static::STATUS_UNINSTALLED; + } + break; + } + + if (!$this->getExtensionManager()->isInstalled($this)) { + return static::STATUS_UNPACKED; + } + + return static::STATUS_INSTALLED; + } + + protected function guessPackagePath(string $code): ?string + { + return match ($this->type) { + static::TYPE_PLUGIN => plugins_path(str_replace('.', '/', $code)), + static::TYPE_THEME => themes_path($code), + static::TYPE_MODULE => base_path('modules/' . $code), + default => null, + }; + } + + protected function guessCodeFromPath(string $path): ?string + { + return match ($this->type) { + static::TYPE_PLUGIN => str_replace('/', '.', ltrim(Str::after($path, basename(plugins_path())), '/')), + static::TYPE_THEME => Str::after($path, themes_path()), + static::TYPE_MODULE => Str::after($path, base_path('modules/')), + default => null, + }; + } + + protected function relativePath(string $path): string + { + return ltrim(Str::after($path, match ($this->type) { + static::TYPE_PLUGIN, static::TYPE_THEME => base_path(), + static::TYPE_MODULE => base_path('modules'), + }), '/'); + } +} diff --git a/modules/system/classes/extensions/ModuleManager.php b/modules/system/classes/extensions/ModuleManager.php index d426ea82f..d47b43a8c 100644 --- a/modules/system/classes/extensions/ModuleManager.php +++ b/modules/system/classes/extensions/ModuleManager.php @@ -2,47 +2,50 @@ namespace System\Classes\Extensions; -class ModuleManager implements WinterExtension +class ModuleManager implements ExtensionManager { - public function install(): static + public function list(): array { - // TODO: Implement install() method. - return $this; + // TODO: Implement list() method. } - public function uninstall(): static + public function create(): WinterExtension { - // TODO: Implement uninstall() method. - return $this; + // TODO: Implement create() method. } - public function enable(): static + public function install(WinterExtension|string $extension): WinterExtension + { + // TODO: Implement install() method. + } + + public function enable(WinterExtension|string $extension): mixed { // TODO: Implement enable() method. - return $this; } - public function disable(): static + public function disable(WinterExtension|string $extension): mixed { // TODO: Implement disable() method. - return $this; } - public function rollback(): static + public function update(WinterExtension|string $extension): mixed { - // TODO: Implement rollback() method. - return $this; + // TODO: Implement update() method. } - public function refresh(): static + public function refresh(WinterExtension|string $extension): mixed { // TODO: Implement refresh() method. - return $this; } - public function update(): static + public function rollback(WinterExtension|string $extension, string $targetVersion): mixed { - // TODO: Implement update() method. - return $this; + // TODO: Implement rollback() method. + } + + public function uninstall(WinterExtension|string $extension): mixed + { + // TODO: Implement uninstall() method. } } diff --git a/modules/system/classes/extensions/ModuleServiceProvider.php b/modules/system/classes/extensions/ModuleServiceProvider.php index e0598b8a0..0f194c493 100644 --- a/modules/system/classes/extensions/ModuleServiceProvider.php +++ b/modules/system/classes/extensions/ModuleServiceProvider.php @@ -97,37 +97,37 @@ protected function loadConfigFrom($path, $namespace) $config->package($namespace, $path); } - public function install(): static + public function extensionInstall(): static { // TODO: Implement install() method. } - public function uninstall(): static + public function extensionUninstall(): static { // TODO: Implement uninstall() method. } - public function enable(): static + public function extensionEnable(): static { // TODO: Implement enable() method. } - public function disable(): static + public function extensionDisable(): static { // TODO: Implement disable() method. } - public function rollback(): static + public function extensionRollback(): static { // TODO: Implement rollback() method. } - public function refresh(): static + public function extensionRefresh(): static { // TODO: Implement refresh() method. } - public function update(): static + public function extensionUpdate(): static { // TODO: Implement update() method. } diff --git a/modules/system/classes/extensions/PluginBase.php b/modules/system/classes/extensions/PluginBase.php index 2608a3a7a..86e904343 100644 --- a/modules/system/classes/extensions/PluginBase.php +++ b/modules/system/classes/extensions/PluginBase.php @@ -535,43 +535,43 @@ public function checkDependencies(PluginManager $manager): bool return true; } - public function install(): static + public function extensionInstall(): static { // TODO: Implement install() method. return $this; } - public function uninstall(): static + public function extensionUninstall(): static { // TODO: Implement uninstall() method. return $this; } - public function enable(): static + public function extensionEnable(): static { // TODO: Implement enable() method. return $this; } - public function disable(): static + public function extensionDisable(): static { // TODO: Implement disable() method. return $this; } - public function rollback(): static + public function extensionRollback(): static { // TODO: Implement rollback() method. return $this; } - public function refresh(): static + public function extensionRefresh(): static { // TODO: Implement refresh() method. return $this; } - public function update(): static + public function extensionUpdate(): static { // TODO: Implement update() method. return $this; diff --git a/modules/system/classes/extensions/PluginManager.php b/modules/system/classes/extensions/PluginManager.php index e80caf14d..c689cc74a 100644 --- a/modules/system/classes/extensions/PluginManager.php +++ b/modules/system/classes/extensions/PluginManager.php @@ -16,7 +16,9 @@ use System\Classes\ComposerManager; use System\Classes\SettingsManager; use System\Classes\UpdateManager; +use System\Classes\VersionManager; use System\Models\PluginVersion; +use Winter\Storm\Exception\ApplicationException; use Winter\Storm\Exception\SystemException; use Winter\Storm\Foundation\Application; use Winter\Storm\Support\ClassLoader; @@ -34,6 +36,8 @@ class PluginManager implements ExtensionManager { use \Winter\Storm\Support\Traits\Singleton; + public const EXTENSION_NAME = 'plugin'; + // // Disabled by system // @@ -1121,9 +1125,42 @@ public function create(): WinterExtension // TODO: Implement create() method. } - public function install(WinterExtension|string $extension): WinterExtension + public function install(ExtensionSource|WinterExtension|string $extension): WinterExtension + { + // Insure the in memory plugins match those on disk + $this->loadPlugins(); + + // Get the plugin code from input and then update the plugin + if (!($code = $this->resolveExtensionCode($extension)) || !VersionManager::instance()->updatePlugin($code)) { + throw new ApplicationException('Unable to update plugin: ' . $code); + } + + // Force a refresh of the plugin + $this->refreshPlugin($code); + + // Return an instance of the plugin + return $this->findByIdentifier($code); + } + + public function isInstalled(ExtensionSource|WinterExtension|string $extension): bool { - // TODO: Implement install() method. + if ( + !($code = $this->resolveExtensionCode($extension)) + || VersionManager::instance()->getCurrentVersion($code) === '0' + ) { + return false; + } + + return true; + } + + public function getExtension(WinterExtension|ExtensionSource|string $extension): ?WinterExtension + { + if (!($code = $this->resolveExtensionCode($extension))) { + return null; + } + + return $this->findByIdentifier($code); } public function enable(WinterExtension|string $extension): mixed @@ -1155,4 +1192,19 @@ public function uninstall(WinterExtension|string $extension): mixed { // TODO: Implement uninstall() method. } + + protected function resolveExtensionCode(ExtensionSource|WinterExtension|string $extension): ?string + { + if (is_string($extension)) { + return $this->getNormalizedIdentifier($extension); + } + if ($extension instanceof ExtensionSource) { + return $this->getNormalizedIdentifier($extension->getCode()); + } + if ($extension instanceof WinterExtension) { + return $extension->getPluginIdentifier(); + } + + return null; + } } diff --git a/modules/system/classes/extensions/WinterExtension.php b/modules/system/classes/extensions/WinterExtension.php index 9bd77bb17..43a460f78 100644 --- a/modules/system/classes/extensions/WinterExtension.php +++ b/modules/system/classes/extensions/WinterExtension.php @@ -4,13 +4,15 @@ interface WinterExtension { - public function install(): static; - public function uninstall(): static; - public function enable(): static; - public function disable(): static; - public function rollback(): static; - public function refresh(): static; - public function update(): static; + public function extensionInstall(): static; + public function extensionUninstall(): static; + public function extensionEnable(): static; + public function extensionDisable(): static; + public function extensionRollback(): static; + public function extensionRefresh(): static; + + public function extensionUpdate(): static; + // public function freeze(): WinterExtension; // public function unfreeze(): WinterExtension;