From 8593b0b89c5f6523ec21c8e20b9a83a8d64daebf Mon Sep 17 00:00:00 2001 From: Jack Wilkinson Date: Fri, 3 Jan 2025 15:32:35 +0000 Subject: [PATCH] Added wip --- modules/cms/classes/Theme.php | 7 + modules/cms/classes/ThemeManager.php | 37 +++- modules/system/classes/UpdateManager.php | 158 ++++-------------- .../system/classes/core/MarketPlaceApi.php | 83 +++++++-- .../core/UpdateManagerCoreManagerTrait.php | 104 ------------ .../core/UpdateManagerFileSystemTrait.php | 67 -------- .../UpdateManagerPluginInstallerTrait.php | 103 ------------ .../core/UpdateManagerThemeInstallerTrait.php | 11 -- .../extensions/ExtensionManagerInterface.php | 94 +++++++++-- .../classes/extensions/ModuleManager.php | 54 ++++-- .../classes/extensions/PluginManager.php | 149 ++++++++++------- 11 files changed, 350 insertions(+), 517 deletions(-) delete mode 100644 modules/system/classes/core/UpdateManagerCoreManagerTrait.php delete mode 100644 modules/system/classes/core/UpdateManagerFileSystemTrait.php delete mode 100644 modules/system/classes/core/UpdateManagerPluginInstallerTrait.php delete mode 100644 modules/system/classes/core/UpdateManagerThemeInstallerTrait.php diff --git a/modules/cms/classes/Theme.php b/modules/cms/classes/Theme.php index e7b3a1f6e..4e81bf16e 100644 --- a/modules/cms/classes/Theme.php +++ b/modules/cms/classes/Theme.php @@ -15,12 +15,14 @@ use Winter\Storm\Halcyon\Datasource\DatasourceInterface; use Winter\Storm\Halcyon\Datasource\DbDatasource; use Winter\Storm\Halcyon\Datasource\FileDatasource; +use Winter\Storm\Packager\Composer; use Winter\Storm\Support\Facades\Config; use Winter\Storm\Support\Facades\Event; use Winter\Storm\Support\Facades\File; use Winter\Storm\Support\Facades\Url; use Winter\Storm\Support\Facades\Yaml; use Winter\Storm\Support\Str; +use Winter\Storm\Support\Traits\HasComposerPackage; /** * This class represents the CMS theme. @@ -32,6 +34,8 @@ */ class Theme extends CmsObject implements WinterExtension { + use HasComposerPackage; + /** * @var string Specifies the theme directory name. */ @@ -74,10 +78,13 @@ public static function load($dirName, $file = null): self $theme = new static; $theme->setDirName($dirName); $theme->registerHalcyonDatasource(); + if (App::runningInBackend()) { $theme->registerBackendLocalization(); } + $theme->setComposerPackage(Composer::getPackageInfoByPath($theme->getPath())); + return $theme; } diff --git a/modules/cms/classes/ThemeManager.php b/modules/cms/classes/ThemeManager.php index 23a7c8796..22d465759 100644 --- a/modules/cms/classes/ThemeManager.php +++ b/modules/cms/classes/ThemeManager.php @@ -12,6 +12,7 @@ use Winter\Storm\Foundation\Extension\WinterExtension; use System\Models\Parameter; use Winter\Storm\Exception\ApplicationException; +use Winter\Storm\Packager\Composer; use Winter\Storm\Support\Facades\File; /** @@ -60,9 +61,8 @@ public function findByDirName($dirName) public function list(): array { - $themes = Theme::all(); return array_combine( - array_map(fn ($theme) => $theme->getIdentifier(), $themes), + array_map(fn ($theme) => $theme->getIdentifier(), $themes = Theme::all()), $themes ); } @@ -148,7 +148,7 @@ public function rollback(WinterExtension|string|null $extension = null, ?string * @return mixed * @throws ApplicationException */ - public function uninstall(WinterExtension|string|null $theme = null): mixed + public function uninstall(WinterExtension|string|null $theme = null, bool $noRollback = false, bool $preserveFiles = false): mixed { if (!$theme) { return false; @@ -168,7 +168,7 @@ public function uninstall(WinterExtension|string|null $theme = null): mixed * Delete from file system */ $themePath = $theme->getPath(); - if (File::isDirectory($themePath)) { + if (File::isDirectory($themePath) && !$preserveFiles) { File::deleteDirectory($themePath); } @@ -201,11 +201,36 @@ public function deleteTheme($theme): mixed public function availableUpdates(WinterExtension|string|null $extension = null): ?array { - // TODO: Implement availableUpdates() method. + $toCheck = $extension ? [$this->get($extension)] : $this->list(); + + $composerUpdates = Composer::getAvailableUpdates(); + + $updates = []; + foreach ($toCheck as $theme) { + if ($theme->getComposerPackageName()) { + if (isset($composerUpdates[$theme->getComposerPackageName()])) { + $updates[$theme->getIdentifier()] = [ + 'from' => $composerUpdates[$theme->getComposerPackageName()][0], + 'to' => $composerUpdates[$theme->getComposerPackageName()][1], + ]; + } + continue; + } + // @TODO: Add market place support for updates + } + + return $updates; } + /** + * @throws ApplicationException + */ public function tearDown(): static { - // TODO: Implement tearDown() method. + foreach ($this->list() as $theme) { + $this->uninstall($theme); + } + + return $this; } } diff --git a/modules/system/classes/UpdateManager.php b/modules/system/classes/UpdateManager.php index aae52de70..2a5569a8e 100644 --- a/modules/system/classes/UpdateManager.php +++ b/modules/system/classes/UpdateManager.php @@ -8,11 +8,14 @@ use Illuminate\Database\Migrations\DatabaseMigrationRepository; use Illuminate\Database\Migrations\Migrator; use Illuminate\Support\Facades\App; +use Mix\TestA\Plugin; use System\Classes\Core\MarketPlaceApi; +use System\Classes\Extensions\ModuleManager; use System\Classes\Extensions\PluginManager; use System\Helpers\Cache as CacheHelper; use System\Models\Parameter; use Winter\Storm\Exception\ApplicationException; +use Winter\Storm\Exception\SystemException; use Winter\Storm\Support\Facades\Config; use Winter\Storm\Support\Facades\Schema; @@ -27,62 +30,18 @@ class UpdateManager { use \Winter\Storm\Support\Traits\Singleton; - use \System\Classes\Core\UpdateManagerFileSystemTrait; - use \System\Classes\Core\UpdateManagerCoreManagerTrait; - use \System\Classes\Core\UpdateManagerPluginInstallerTrait; - use \System\Classes\Core\UpdateManagerThemeInstallerTrait; - - protected PluginManager $pluginManager; - protected ThemeManager $themeManager; - protected MarketPlaceApi $api; - protected Migrator $migrator; - protected DatabaseMigrationRepository $repository; /** * If set to true, core updates will not be downloaded or extracted. */ protected bool $disableCoreUpdates = false; - /** - * Array of messages returned by migrations / seeders. Returned at the end of the update process. - */ - protected array $messages = []; - /** * Initialize this singleton. */ protected function init() { $this->disableCoreUpdates = Config::get('cms.disableCoreUpdates', false); - - $this->bindContainerObjects() - ->setTempDirectory(temp_path()) - ->setBaseDirectory(base_path()); - } - - /** - * These objects are "soft singletons" and may be lost when - * the IoC container reboots. This provides a way to rebuild - * for the purposes of unit testing. - */ - public function bindContainerObjects(bool $refresh = false): static - { - $this->api = isset($this->api) && !$refresh - ? $this->api - : MarketPlaceApi::instance(); - - $this->pluginManager = isset($this->pluginManager) && !$refresh - ? $this->pluginManager - : PluginManager::instance(); - - $this->themeManager = isset($this->themeManager) && !$refresh - ? $this->themeManager - : (class_exists(ThemeManager::class) ? ThemeManager::instance() : null); - - $this->migrator = App::make('migrator'); - $this->repository = App::make('migration.repository'); - - return $this; } public function isSystemSetup(): bool @@ -95,37 +54,6 @@ public function getMigrationTableName(): string return Config::get('database.migrations', 'migrations'); } - /** - * Creates the migration table and updates - * @throws ApplicationException - */ - public function update(): static - { - $firstUp = $this->isSystemSetup(); - - $modules = Config::get('cms.loadModules', []); - - if ($firstUp) { - $this->setupMigrations(); - } - - $this->migrateModules($modules); - $plugins = $this->mapPluginReplacements(); - - if ($firstUp) { - $this->seedModules($modules); - } - - $this->updatePlugins($plugins); - - Parameter::set('system::update.count', 0); - CacheHelper::clear(); - - $this->generatePluginReplacementNotices(); - - return $this; - } - /** * Checks for new updates and returns the amount of unapplied updates. * Only requests from the server at a set interval (retry timer). @@ -163,38 +91,37 @@ public function check(bool $force = false): int return $newCount; } + public function availableUpdates(): array + { + return [ + 'modules' => ModuleManager::instance()->availableUpdates(), + 'plugins' => PluginManager::instance()->availableUpdates(), + 'themes' => ThemeManager::instance()->availableUpdates(), + ]; + } + /** - * Roll back all modules and plugins. + * @throws ApplicationException + * @throws SystemException */ - public function tearDownTheSystem(): static + public function update(): static { - /* - * Rollback plugins - */ - $plugins = array_reverse($this->pluginManager->getAllPlugins()); - foreach ($plugins as $name => $plugin) { - $this->rollbackPlugin($name); - } - - /* - * Register module migration files - */ - $paths = []; - $modules = Config::get('cms.loadModules', []); + ModuleManager::instance()->update(); + PluginManager::instance()->update(); + ThemeManager::instance()->update(); - foreach ($modules as $module) { - $paths[] = base_path() . '/modules/' . strtolower($module) . '/database/migrations'; - } - - while (true) { - $rolledBack = $this->migrator->rollback($paths, ['pretend' => false]); - - if (count($rolledBack) == 0) { - break; - } - } + return $this; + } - Schema::dropIfExists($this->getMigrationTableName()); + /** + * Roll back all modules and plugins. + * @throws ApplicationException + */ + public function tearDownTheSystem(): static + { + ThemeManager::instance()->tearDown(); + PluginManager::instance()->tearDown(); + ModuleManager::instance()->tearDown(); return $this; } @@ -257,31 +184,4 @@ public function setBuild(string $build, ?string $hash = null, bool $modified = f Parameter::set($params); } - - protected function message(string|object $class, string $format, mixed ...$args): static - { - $this->messages[] = [ - 'class' => is_object($class) ? get_class($class) : $class, - 'message' => sprintf($format, ...$args), - 'type' => 'info', - ]; - - return $this; - } - - protected function error(string|object $class, string $format, mixed ...$args): static - { - $this->messages[] = [ - 'class' => is_object($class) ? get_class($class) : $class, - 'message' => sprintf($format, ...$args), - 'type' => 'error', - ]; - - return $this; - } - - public function getMessages(): array - { - return $this->messages; - } } diff --git a/modules/system/classes/core/MarketPlaceApi.php b/modules/system/classes/core/MarketPlaceApi.php index c4ab4acb1..ecf116d3f 100644 --- a/modules/system/classes/core/MarketPlaceApi.php +++ b/modules/system/classes/core/MarketPlaceApi.php @@ -23,7 +23,6 @@ class MarketPlaceApi { use Singleton; - use UpdateManagerFileSystemTrait; use InteractsWithZip; public const PRODUCT_CACHE_KEY = 'system-updates-product-details'; @@ -33,14 +32,6 @@ class MarketPlaceApi public const REQUEST_THEME_DETAIL = 'theme/detail'; public const REQUEST_PROJECT_DETAIL = 'project/detail'; - /** - * Cache of gateway products - */ - protected array $productCache = [ - 'theme' => [], - 'plugin' => [], - ]; - /** * Secure API Key */ @@ -51,13 +42,33 @@ class MarketPlaceApi */ protected ?string $secret = null; + /** + * @var string Used during download of files + */ + protected string $tempDirectory; + + /** + * @var string Directs the UpdateManager where to unpack archives to + */ + protected string $baseDirectory; + + /** + * Cache of gateway products + */ + protected array $productCache = [ + 'theme' => [], + 'plugin' => [], + ]; + + public function init() { if (Cache::has(static::PRODUCT_CACHE_KEY)) { $this->productCache = Cache::get(static::PRODUCT_CACHE_KEY); } - $this->setTempDirectory(temp_path()); + $this->setTempDirectory(temp_path()) + ->setBaseDirectory(base_path()); } /** @@ -69,6 +80,50 @@ public function setSecurity(string $key, string $secret): void $this->secret = $secret; } + /** + * Set the temp directory used by the UpdateManager. Defaults to `temp_path()` but can be overwritten if required. + * + * @param string $tempDirectory + * @return $this + */ + public function setTempDirectory(string $tempDirectory): static + { + $this->tempDirectory = $tempDirectory; + + // Ensure temp directory exists + if (!File::isDirectory($this->tempDirectory) && File::isWritable($this->tempDirectory)) { + File::makeDirectory($this->tempDirectory, recursive: true); + } + + return $this; + } + + /** + * Set the base directory used by the UpdateManager. Defaults to `base_path()` but can be overwritten if required. + * + * @param string $baseDirectory + * @return $this + */ + public function setBaseDirectory(string $baseDirectory): static + { + $this->baseDirectory = $baseDirectory; + + // Ensure temp directory exists + if (!File::isDirectory($this->baseDirectory)) { + throw new \RuntimeException('The base directory "' . $this->baseDirectory . '" does not exist.'); + } + + return $this; + } + + /** + * Calculates a file path for a file code + */ + protected function getFilePath(string $fileCode): string + { + return $this->tempDirectory . '/' . md5($fileCode) . '.arc'; + } + /** * Handles fetching data for system info stuff maybe * @@ -400,6 +455,14 @@ public function extractTheme(string $name, string $hash): void $this->extractArchive($filePath, themes_path()); } + /** + * Looks up a plugin from the update server. + */ + public function requestPluginDetails(string $name): array + { + return $this->api->fetch('plugin/detail', ['name' => $name]); + } + /** * Downloads a plugin from the update server. * @param bool $installation Indicates whether this is a plugin installation request. diff --git a/modules/system/classes/core/UpdateManagerCoreManagerTrait.php b/modules/system/classes/core/UpdateManagerCoreManagerTrait.php deleted file mode 100644 index 0e8b7a16f..000000000 --- a/modules/system/classes/core/UpdateManagerCoreManagerTrait.php +++ /dev/null @@ -1,104 +0,0 @@ -lists('version', 'code'); - $names = $installed->lists('name', 'code'); - $icons = $installed->lists('icon', 'code'); - $frozen = $installed->lists('is_frozen', 'code'); - $updatable = $installed->lists('is_updatable', 'code'); - $build = Parameter::get('system::core.build'); - $themes = []; - - if ($this->themeManager) { - $themes = array_keys($this->themeManager->getInstalled()); - } - - $params = [ - 'core' => $this->getHash(), - 'plugins' => serialize($versions), - 'themes' => serialize($themes), - 'build' => $build, - 'force' => $force - ]; - - $result = $this->api->fetch('core/update', $params); - $updateCount = (int) array_get($result, 'update', 0); - - /* - * Inject known core build - */ - if ($core = array_get($result, 'core')) { - $core['old_build'] = Parameter::get('system::core.build'); - $result['core'] = $core; - } - - /* - * Inject the application's known plugin name and version - */ - $plugins = []; - foreach (array_get($result, 'plugins', []) as $code => $info) { - $info['name'] = $names[$code] ?? $code; - $info['old_version'] = $versions[$code] ?? false; - $info['icon'] = $icons[$code] ?? false; - - /* - * If a plugin has updates frozen, or cannot be updated, - * do not add to the list and discount an update unit. - */ - if ( - (isset($frozen[$code]) && $frozen[$code]) || - (isset($updatable[$code]) && !$updatable[$code]) - ) { - $updateCount = max(0, --$updateCount); - } else { - $plugins[$code] = $info; - } - } - $result['plugins'] = $plugins; - - /* - * Strip out themes that have been installed before - */ - if ($this->themeManager) { - $themes = []; - foreach (array_get($result, 'themes', []) as $code => $info) { - if (!$this->themeManager->isInstalled($code)) { - $themes[$code] = $info; - } - } - $result['themes'] = $themes; - } - - /* - * If there is a core update and core updates are disabled, - * remove the entry and discount an update unit. - */ - if (array_get($result, 'core') && $this->disableCoreUpdates) { - $updateCount = max(0, --$updateCount); - unset($result['core']); - } - - /* - * Recalculate the update counter - */ - $updateCount += count($themes); - $result['hasUpdates'] = $updateCount > 0; - $result['update'] = $updateCount; - Parameter::set('system::update.count', $updateCount); - - return $result; - } -} diff --git a/modules/system/classes/core/UpdateManagerFileSystemTrait.php b/modules/system/classes/core/UpdateManagerFileSystemTrait.php deleted file mode 100644 index 4e022476f..000000000 --- a/modules/system/classes/core/UpdateManagerFileSystemTrait.php +++ /dev/null @@ -1,67 +0,0 @@ -tempDirectory = $tempDirectory; - - // Ensure temp directory exists - if (!File::isDirectory($this->tempDirectory) && File::isWritable($this->tempDirectory)) { - File::makeDirectory($this->tempDirectory, recursive: true); - } - - return $this; - } - - /** - * Set the base directory used by the UpdateManager. Defaults to `base_path()` but can be overwritten if required. - * - * @param string $baseDirectory - * @return $this - */ - public function setBaseDirectory(string $baseDirectory): static - { - $this->baseDirectory = $baseDirectory; - - // Ensure temp directory exists - if (!File::isDirectory($this->baseDirectory)) { - throw new \RuntimeException('The base directory "' . $this->baseDirectory . '" does not exist.'); - } - - return $this; - } - - /** - * Calculates a file path for a file code - */ - protected function getFilePath(string $fileCode): string - { - return $this->tempDirectory . '/' . md5($fileCode) . '.arc'; - } - - -} diff --git a/modules/system/classes/core/UpdateManagerPluginInstallerTrait.php b/modules/system/classes/core/UpdateManagerPluginInstallerTrait.php deleted file mode 100644 index 771070bf2..000000000 --- a/modules/system/classes/core/UpdateManagerPluginInstallerTrait.php +++ /dev/null @@ -1,103 +0,0 @@ - $plugin) { - $this->updatePlugin($code); - } - - return $this; - } - - /** - * Runs update on a single plugin - */ - public function updatePlugin(string $name): static - { - // Update the plugin database and version - if (!($plugin = $this->pluginManager->findByIdentifier($name))) { - $this->pluginManager->getOutput()->info(sprintf('Unable to find plugin %s', $name)); - return $this; - } - - $this->pluginManager->getOutput()->info(sprintf('Migrating %s (%s) plugin...', Lang::get($plugin->pluginDetails()['name']), $name)); - - $this->pluginManager->versionManager()->updatePlugin($plugin); - - return $this; - } - - /** - * Rollback an existing plugin - * - * @param string|null $stopOnVersion If this parameter is specified, the process stops once the provided version number is reached - * @throws ApplicationException if the provided stopOnVersion cannot be found in the database - */ - public function rollbackPlugin(string $name, string $stopOnVersion = null): static - { - // Remove the plugin database and version - if (!($plugin = $this->pluginManager->findByIdentifier($name)) - && $this->pluginManager->versionManager()->purgePlugin($name) - ) { - $this->pluginManager->getOutput()->info(sprintf('%s purged from database', $name)); - return $this; - } - - if ($stopOnVersion && !$this->pluginManager->versionManager()->hasDatabaseVersion($plugin, $stopOnVersion)) { - throw new ApplicationException(Lang::get('system::lang.updates.plugin_version_not_found')); - } - - if ($this->pluginManager->versionManager()->removePlugin($plugin, $stopOnVersion, true)) { - $this->pluginManager->getOutput()->info(sprintf('%s rolled back', $name)); - - if ($currentVersion = $this->pluginManager->versionManager()->getCurrentVersion($plugin)) { - $this->message( - $this, - 'Current Version: %s (%s)', - $currentVersion, - $this->pluginManager->versionManager()->getCurrentVersionNote($plugin) - ); - } - - return $this; - } - - $this->error($this, sprintf('Unable to find plugin %s', $name)); - - return $this; - } - - /** - * Looks up a plugin from the update server. - */ - public function requestPluginDetails(string $name): array - { - return $this->api->fetch('plugin/detail', ['name' => $name]); - } - - public function request(string $type, string $info, string $name) - { - if (!in_array($type, ['plugin'])) { - throw new ApplicationException('Invalid request type.'); - } - - if (!in_array($info, ['detail', 'content'])) { - throw new ApplicationException('Invalid request info.'); - } - - return $this->api->fetch($type . '/' . $info, ['name' => $name]); - } - - - -} diff --git a/modules/system/classes/core/UpdateManagerThemeInstallerTrait.php b/modules/system/classes/core/UpdateManagerThemeInstallerTrait.php deleted file mode 100644 index bb2f10545..000000000 --- a/modules/system/classes/core/UpdateManagerThemeInstallerTrait.php +++ /dev/null @@ -1,11 +0,0 @@ - */ public function list(): array; + /** + * Creates a new extension with the code provided + * + * @param string $extension + * @return WinterExtension + */ public function create(string $extension): WinterExtension; /** + * Installs an ExtensionSource or Extension, if extension is registered but not installed then installation steps + * are ran + * * @throws ApplicationException If the installation fails */ public function install(ExtensionSource|WinterExtension|string $extension ): WinterExtension; + /** + * Validates if an extension is installed or not + * + * @param ExtensionSource|WinterExtension|string $extension + * @return bool + */ public function isInstalled(ExtensionSource|WinterExtension|string $extension): bool; + /** + * Returns an extension + * + * @param ExtensionSource|WinterExtension|string $extension + * @return WinterExtension|null + */ public function get(ExtensionSource|WinterExtension|string $extension): ?WinterExtension; + /** + * Clears flag passed, if all flags are removed the extension will be enabled + * + * @param WinterExtension|string $extension + * @param string|bool $flag + * @return mixed + */ public function enable(WinterExtension|string $extension, string|bool $flag = self::DISABLED_BY_USER): mixed; + /** + * Disables the extension using the flag provided + * + * @param WinterExtension|string $extension + * @param string|bool $flag + * @return mixed + */ public function disable(WinterExtension|string $extension, string|bool $flag = self::DISABLED_BY_USER): mixed; + /** + * Updates the extension, by default fetching any remote updates prior to running migrations + * + * @param WinterExtension|string|null $extension + * @param bool $migrationsOnly + * @return mixed + */ public function update(WinterExtension|string|null $extension = null, bool $migrationsOnly = false): mixed; + /** + * Fetches updates available for extension, if null is passed then returns all updates for registered extensions + * + * @param WinterExtension|string|null $extension + * @return array|null + */ public function availableUpdates(WinterExtension|string|null $extension = null): ?array; + /** + * Rollback and re-apply any migrations provided by the extension + * + * @param WinterExtension|string|null $extension + * @return mixed + */ public function refresh(WinterExtension|string|null $extension = null): mixed; + /** + * Rollback an extension to a specific version + * + * @param WinterExtension|string|null $extension + * @param string|null $targetVersion + * @return mixed + */ public function rollback(WinterExtension|string|null $extension = null, ?string $targetVersion = null): mixed; - public function uninstall(WinterExtension|string|null $extension = null): mixed; + /** + * Remove a single extension + * + * @param WinterExtension|string $extension + * @return mixed + */ + public function uninstall(WinterExtension|string $extension, bool $noRollback = false, bool $preserveFiles = false): mixed; + + /** + * Completely uninstall all extensions managed by this manager + * + * @return static + */ + public function tearDown(): static; } diff --git a/modules/system/classes/extensions/ModuleManager.php b/modules/system/classes/extensions/ModuleManager.php index 7aa8004e9..1af72b10e 100644 --- a/modules/system/classes/extensions/ModuleManager.php +++ b/modules/system/classes/extensions/ModuleManager.php @@ -6,6 +6,7 @@ use Illuminate\Database\Migrations\DatabaseMigrationRepository; use Illuminate\Database\Migrations\Migrator; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\File; use System\Classes\Extensions\Source\ExtensionSource; use System\Classes\UpdateManager; use System\Helpers\Cache as CacheHelper; @@ -181,21 +182,38 @@ public function rollback(WinterExtension|string|null $extension = null, ?string return true; } - public function uninstall(WinterExtension|string|null $extension = null): mixed + public function uninstall(WinterExtension|string $extension, bool $noRollback = false, bool $preserveFiles = false): mixed { - $modules = $this->getModuleList($extension); - foreach ($modules as $module) { + if (!($module = $this->resolve($extension))) { + throw new ApplicationException(sprintf( + 'Unable to uninstall module: %s', + is_string($extension) ? $extension : $extension->getIdentifier() + )); + } + + if (!$noRollback) { $this->rollback($module); } - // System uninstall - if (!$extension) { - Schema::dropIfExists(UpdateManager::instance()->getMigrationTableName()); + if (!$preserveFiles) { + // Modules probably should not be removed + // File::deleteDirectory($module->getPath()); } return true; } + public function tearDown(): static + { + foreach ($this->list() as $module) { + $this->uninstall($module); + } + + Schema::dropIfExists(UpdateManager::instance()->getMigrationTableName()); + + return $this; + } + public function isInstalled(WinterExtension|ExtensionSource|string $extension): bool { return !!$this->get($extension); @@ -226,20 +244,20 @@ public function get(WinterExtension|ExtensionSource|string $extension): ?WinterE public function availableUpdates(WinterExtension|string|null $extension = null): ?array { - $updates = []; - $composerUpdates = null; - foreach ($this->list() as $name => $module) { - if ($composerPackage = $module->getComposerPackageName()) { - if (!$composerUpdates) { - $composerUpdates = Composer::getAvailableUpdates(); - } + $toCheck = $extension ? [$this->get($extension)] : $this->list(); - if (isset($composerUpdates[$composerPackage])) { - $updates[$name] = $composerUpdates[$composerPackage]; - } - } else { - // @TODO: api check + $composerUpdates = Composer::getAvailableUpdates(); + + $updates = []; + foreach ($toCheck as $module) { + if (!$module->getComposerPackageName() || !isset($composerUpdates[$module->getComposerPackageName()])) { + continue; } + + $updates[$module->getIdentifier()] = [ + 'from' => $composerUpdates[$module->getComposerPackageName()][0], + 'to' => $composerUpdates[$module->getComposerPackageName()][1], + ]; } return $updates; diff --git a/modules/system/classes/extensions/PluginManager.php b/modules/system/classes/extensions/PluginManager.php index 5765614c1..3b6ab1fdb 100644 --- a/modules/system/classes/extensions/PluginManager.php +++ b/modules/system/classes/extensions/PluginManager.php @@ -17,8 +17,11 @@ use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use System\Classes\ComposerManager; +use System\Classes\Core\MarketPlaceApi; use System\Classes\Extensions\Source\ExtensionSource; use System\Classes\SettingsManager; +use System\Classes\UpdateManager; +use System\Models\Parameter; use System\Models\PluginVersion; use Winter\Storm\Exception\ApplicationException; use Winter\Storm\Exception\SystemException; @@ -38,8 +41,6 @@ */ class PluginManager extends ExtensionManager implements ExtensionManagerInterface { - public const EXTENSION_NAME = 'plugin'; - /** * The application instance, since Plugins are an extension of a Service Provider */ @@ -279,70 +280,80 @@ public function disable(WinterExtension|string $extension, string|bool $flag = s */ public function update(WinterExtension|string|null $extension = null, bool $migrationsOnly = false): ?bool { - if (!($code = $this->resolveIdentifier($extension))) { - return null; + $plugins = []; + // If null, load all plugins + if (!$extension) { + $plugins = $this->list(); } - // Update the plugin database and version - if (!($plugin = $this->findByIdentifier($code))) { - $this->renderComponent(Error::class, sprintf('Unable to find plugin %s', $code)); - return null; + if (!$plugins) { + if (!($resolved = $this->resolve($extension))) { + throw new ApplicationException( + 'Unable to resolve plugin: ' . is_string($extension) ? $extension : $extension->getIdentifier() + ); + } + $plugins = [$resolved->getIdentifier() => $resolved]; } - $pluginName = Lang::get($plugin->pluginDetails()['name']); - - if (!$migrationsOnly) { - if ( - !$this->getPluginRecord($plugin)->is_frozen - && ($composerPackage = $plugin->getComposerPackageName()) - && Composer::updateAvailable($composerPackage) - ) { - $this->renderComponent(Info::class, sprintf( - 'Performing composer update for %s (%s) plugin...', - $pluginName, - $code - )); - - Preserver::instance()->store($plugin); - $update = Composer::update(dryRun: true, package: $composerPackage); - - ($versions = $update->getUpgraded()[$composerPackage] ?? null) - ? $this->renderComponent( - Info::class, - sprintf('Updated plugin %s (%s) from v%s => v%s', $pluginName, $code, $versions[0], $versions[1]) - ) - : $this->renderComponent( - Error::class, - sprintf('Failed to update plugin %s (%s)', $pluginName, $code) + foreach ($plugins as $code => $plugin) { + $pluginName = Lang::get($plugin->pluginDetails()['name']); + if (!$migrationsOnly) { + if ( + !$this->getPluginRecord($plugin)->is_frozen + && ($composerPackage = $plugin->getComposerPackageName()) + && Composer::updateAvailable($composerPackage) + ) { + $this->renderComponent(Info::class, sprintf( + 'Performing composer update for %s (%s) plugin...', + $pluginName, + $code + )); + + Preserver::instance()->store($plugin); + // @TODO: Make this not dry run + $update = Composer::update(dryRun: true, package: $composerPackage); + + ($versions = $update->getUpgraded()[$composerPackage] ?? null) + ? $this->renderComponent(Info::class, sprintf( + 'Updated plugin %s (%s) from v%s => v%s', + $pluginName, + $code, + $versions[0], + $versions[1] + )) + : $this->renderComponent(Error::class, sprintf( + 'Failed to update plugin %s (%s)', + $pluginName, + $code + ) ); - } elseif (false /* Detect if market */) { - Preserver::instance()->store($plugin); - // @TODO: Update files from market + } elseif (false /* Detect if market */) { + Preserver::instance()->store($plugin); + // @TODO: Update files from market + } } - } - $this->renderComponent(Info::class, sprintf('Migrating %s (%s) plugin...', $pluginName, $code)); - $this->versionManager->updatePlugin($plugin); + $this->renderComponent(Info::class, sprintf('Migrating %s (%s) plugin...', $pluginName, $code)); + $this->versionManager->updatePlugin($plugin); - // Ensure any active aliases have their history migrated for replacing plugins - $this->migratePluginReplacements(); + // Ensure any active aliases have their history migrated for replacing plugins + $this->migratePluginReplacements(); + } return true; } - public function migratePluginReplacements(): array + /* + * Replace plugins + */ + public function migratePluginReplacements(): static { - $plugins = $this->list(); - - /* - * Replace plugins - */ - foreach ($plugins as $code => $plugin) { + foreach ($this->list() as $code => $plugin) { if (!($replaces = $plugin->getReplaces())) { continue; } - // TODO: add full support for plugins replacing multiple plugins + // @TODO: add full support for plugins replacing multiple plugins if (count($replaces) > 1) { throw new ApplicationException(Lang::get('system::lang.plugins.replace.multi_install_error')); } @@ -351,7 +362,7 @@ public function migratePluginReplacements(): array } } - return $plugins; + return $this; } public function availableUpdates(WinterExtension|string|null $extension = null): ?array @@ -364,16 +375,14 @@ public function availableUpdates(WinterExtension|string|null $extension = null): foreach ($toCheck as $plugin) { if ($plugin->getComposerPackageName()) { if (isset($composerUpdates[$plugin->getComposerPackageName()])) { - $updates[] = [ - 'name' => $plugin->getPluginIdentifier(), + $updates[$plugin->getPluginIdentifier()] = [ 'from' => $composerUpdates[$plugin->getComposerPackageName()][0], 'to' => $composerUpdates[$plugin->getComposerPackageName()][1], ]; } continue; } - - // @TODO: Check api + // @TODO: Add market place support for updates } return $updates; @@ -441,7 +450,7 @@ public function rollback(WinterExtension|string|null $extension = null, ?string * Completely roll back and delete a plugin from the system. * @throws ApplicationException */ - public function uninstall(WinterExtension|string|null $extension = null, bool $noRollback = false): ?bool + public function uninstall(WinterExtension|string $extension, bool $noRollback = false, bool $preserveFiles = false): ?bool { if (!($code = $this->resolveIdentifier($extension))) { return null; @@ -454,7 +463,9 @@ public function uninstall(WinterExtension|string|null $extension = null, bool $n // Delete from file system if ($pluginPath = self::instance()->getPluginPath($code)) { - File::deleteDirectory($pluginPath); + if (!$preserveFiles) { + File::deleteDirectory($pluginPath); + } // Clear the registration values cache $this->registrationMethodCache = []; @@ -468,10 +479,14 @@ public function uninstall(WinterExtension|string|null $extension = null, bool $n return true; } + /** + * Uninstall all plugins + * @throws ApplicationException + */ public function tearDown(): static { - foreach ($this->getAllPlugins() as $plugin) { - $this->uninstall($plugin); + foreach (array_reverse($this->getAllPlugins()) as $plugin) { + $this->uninstall($plugin, preserveFiles: true); } return $this; @@ -1418,6 +1433,24 @@ public function resolveIdentifier(ExtensionSource|WinterExtension|string $extens return null; } + /** + * @param WinterExtension|string|null $extension + * @return array + * @throws ApplicationException + */ + protected function getPluginList(WinterExtension|string|null $extension = null): array + { + if (!$extension) { + return $this->list(); + } + + if (!($resolved = $this->resolve($extension))) { + throw new ApplicationException('Unable to locate extension'); + } + + return [$resolved->getIdentifier() => $resolved]; + } + /** * Returns an array with all enabled plugins *