Skip to content

Conversation

@jaxwilko
Copy link
Member

@jaxwilko jaxwilko commented Nov 29, 2024

Me and Luke are working on a little refactor of how the UpdateManager works...

It also includes some components from my old composer PR found here: #967

The core idea of this is that there should be consistent interfaces to interact with different things built on top of Winter (i.e. modules, plugins & themes). Each of these elements will now have their class (i.e. Theme, PluginBase, ModuleServiceProvider) will now extend WinterExtension which will allow them to all interact with the update manager and other system functions directly.

In addition PluginManager, ThemeManager & ModuleManager (new) will all implement the ExtensionManager interface, which will allow for a consistant way to interact with all Winter extensions.

I.e. You will be able to call PluginManager::instance()->update($plugin) or ThemeManager::instance()->update($theme).

This also allows us to do some cool stuff like (only support for plugin installing is currently functional)

(new ExtensionSource(
    ExtensionSource::SOURCE_COMPOSER,
    ExtensionSource::TYPE_PLUGIN,
    composerPackage: 'jaxwilko/datamigrator'
))->install();

(new ExtensionSource(
    ExtensionSource::SOURCE_COMPOSER,
    ExtensionSource::TYPE_THEME,
    composerPackage: 'jaxwilko/custom-theme'
))->install();

(new ExtensionSource(
    ExtensionSource::SOURCE_LOCAL,
    ExtensionSource::TYPE_PLUGIN,
    code: 'winter.demo'
))->install();

(new ExtensionSource(
    ExtensionSource::SOURCE_LOCAL,
    ExtensionSource::TYPE_PLUGIN,
    path: 'plugins/winter/demo'
))->install();

By the end of the PR, the UpdateManager may no longer exist as is, instead each ExtensionManager will be responsible for its existing functionality.

Requires storm branch: https://github.com/wintercms/storm/tree/wip/support-uploading-packages-update-manager-changes

Summary by CodeRabbit

  • New Features

    • Added marketplace UI for browsing and installing plugins and themes directly.
    • Added support for uploading plugins and themes as ZIP files.
    • Integrated marketplace API for enhanced package management and discovery.
  • Improvements

    • Redesigned plugin and theme management interface with improved navigation and search capabilities.
    • Updated system updates page with streamlined extension installation workflow.

@jaxwilko jaxwilko marked this pull request as draft November 29, 2024 18:41
@jaxwilko jaxwilko requested a review from LukeTowers November 29, 2024 18:41
"require": {
"php": ">=8.1",
"winter/storm": "dev-develop as 1.2",
"winter/storm": "dev-wip/support-uploading-packages-update-manager-changes as 1.2",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will need to be updated before merge.

@coderabbitai
Copy link

coderabbitai bot commented Oct 29, 2025

Caution

Review failed

Failed to post review comments

Walkthrough

This pull request introduces a comprehensive extension management framework refactoring. It moves plugin/theme/module management classes to a new Extensions namespace, establishes abstract ExtensionManager and ExtensionManagerInterface for unified extension handling, introduces a MarketPlaceApi for marketplace integration, refactors UpdateManager to delegate to specialized managers, reorganizes console commands into sub-namespaces, and adds a Vue-based marketplace UI component for browsing and installing plugins/themes.

Changes

Cohort / File(s) Summary
Namespace & Alias Updates
modules/system/aliases.php, modules/system/classes/MarkupManager.php, modules/system/classes/SettingsManager.php, modules/system/classes/asset/PackageManager.php, modules/backend/classes/AuthManager.php, modules/backend/classes/NavigationManager.php, modules/backend/classes/WidgetManager.php, modules/backend/widgets/Lists.php, modules/cms/classes/ComponentManager.php, modules/cms/widgets/ComponentList.php, modules/system/console/WinterTest.php, modules/system/console/traits/HasPluginArgument.php, modules/system/console/create/scaffold/plugin/plugin.stub, modules/system/tests/README.md
Updated PluginManager and PluginBase import paths from System\Classes to System\Classes\Extensions, and created new aliases mapping old paths to new namespaced locations.
Extension Framework Classes
modules/system/classes/extensions/ExtensionManager.php, modules/system/classes/extensions/ExtensionManagerInterface.php, modules/system/classes/extensions/PluginBase.php, modules/system/classes/extensions/PluginManager.php, modules/system/classes/extensions/PluginVersionManager.php, modules/system/classes/extensions/ModuleManager.php, modules/system/classes/extensions/Preserver.php
Introduced abstract base class ExtensionManager, new ExtensionManagerInterface, moved/refactored PluginBase and PluginManager, renamed VersionManager to PluginVersionManager, added ModuleManager, and introduced Preserver for extension archiving.
Extension Source Classes
modules/system/classes/extensions/source/ExtensionSource.php, modules/system/classes/extensions/source/ComposerSource.php, modules/system/classes/extensions/source/LocalSource.php, modules/system/classes/extensions/source/MarketSource.php
New source abstraction layer for managing extension installation from composer, marketplace, or local sources.
Extension Traits
modules/system/classes/extensions/traits/HasVersionFile.php
New trait for parsing version files from YAML.
Theme Management Refactor
modules/cms/classes/Theme.php, modules/cms/classes/ThemeManager.php
Updated Theme to implement WinterExtension with versioning support; refactored ThemeManager to extend ExtensionManager and implement ExtensionManagerInterface.
Service Providers
modules/backend/ServiceProvider.php, modules/cms/ServiceProvider.php, modules/system/ServiceProvider.php
Consolidated console command registration into batch calls; added console-specific initialization for output handling.
UpdateManager Refactor
modules/system/classes/UpdateManager.php
Significantly simplified UpdateManager by removing detailed server communication and delegation logic; introduced isSystemSetup(), availableUpdates(), update(), and tearDownTheSystem() methods; delegated plugin/theme/module updates to specialized managers.
Marketplace API
modules/system/classes/core/MarketPlaceApi.php, modules/system/classes/core/package-list.json
Introduced new MarketPlaceApi class for marketplace server communication with product caching, HTTP signing, file downloads, and product search; added static product catalog JSON.
Console Command Restructuring
modules/system/console/create/*, modules/system/console/plugin/*, modules/system/console/asset/*
Reorganized console commands into Create and Plugin sub-namespaces; added new PluginInstall command under plugin namespace; updated command implementations to use new manager APIs.
System Console Commands
modules/system/console/WinterDown.php, modules/system/console/WinterInstall.php, modules/system/console/WinterUp.php, modules/system/console/WinterUpdate.php, modules/system/console/WinterUtil.php
Updated to use new manager-based approaches and MarketPlaceApi instead of direct UpdateManager calls.
File Manifest & Configuration
modules/system/classes/FileManifest.php, modules/system/classes/SourceManifest.php
Made constructor parameters explicitly nullable and updated to use Config facade.
Asset Management
modules/system/console/asset/AssetCreate.php, modules/system/console/asset/AssetInstall.php, modules/system/console/asset/fixtures/config/tailwind/tailwind.module.config.js.fixture, modules/system/console/asset/npm/NpmVersion.php
Updated imports and added Tailwind configuration fixture for modules.
System Models
modules/system/models/MailSetting.php, modules/system/models/Parameter.php, modules/system/models/PluginVersion.php
Added latest property to PluginVersion and updated docblock types in MailSetting.
Plugin Version Columns
modules/system/models/pluginversion/columns.yaml, modules/system/models/pluginversion/columns_manage.yaml
Added new UI columns (is_unfrozen, is_enabled, open_link) and removed columns_manage.yaml.
Updates Controller
modules/system/controllers/Updates.php, modules/system/controllers/updates/traits/*
Refactored controller to use new manager APIs; split logic into ManagesMarketplaceProject, ManagesPlugins, and ManagesThemes traits.
Updates Views & Partials
modules/system/controllers/updates/index.php, modules/system/controllers/updates/install.php, modules/system/controllers/updates/*_*.php
Restructured UI from server-rendered forms to marketplace component-driven interface; updated list display, manage toolbar, and install dialogs.
Marketplace UI Components
modules/system/controllers/updates/assets/src/components/MarketPlace.vue, modules/system/controllers/updates/assets/src/components/Pagination.vue, modules/system/controllers/updates/assets/src/components/Product.vue, modules/system/controllers/updates/assets/src/utils/*, modules/system/controllers/updates/assets/src/updates.js, modules/system/controllers/updates/assets/src/tailwind.config.js
Introduced Vue-based marketplace UI with product browsing, filtering, pagination, and installation flows.
Styling & Configuration
modules/system/controllers/updates/assets/src/updates.css, modules/system/controllers/updates/assets/.eslintignore, modules/system/controllers/updates/config_list.yaml, modules/system/controllers/updates/config_manage_list.yaml, modules/system/controllers/updates/form.plugin_upload.yaml, modules/system/controllers/updates/form.theme_upload.yaml
Added Tailwind CSS configuration and styling for updates UI; configured list behavior and upload forms.
Language & Dependencies
modules/system/lang/en/lang.php, modules/system/package.json, modules/system/postcss.config.mjs, modules/system/reportwidgets/Status.php
Added translation keys for marketplace and plugin management; added Vue, Tailwind, and PostCSS dependencies; added PostCSS configuration.
Helper Classes
modules/system/console/BaseScaffoldCommand.php, modules/backend/classes/BackendController.php
Minor output formatting and documentation updates.
Dependency Updates
composer.json
Updated winter/storm dependency branch reference for package upload support.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant MarketplaceUI as Marketplace UI<br/>(Vue Component)
    participant UpdatesController as Updates<br/>Controller
    participant ExtensionSource
    participant ExtensionManager as Extension<br/>Manager
    participant MarketPlaceApi
    participant FileSystem

    User->>MarketplaceUI: Browse/Search Products
    MarketplaceUI->>UpdatesController: onGetMarketplaceListings()
    UpdatesController->>MarketPlaceApi: requestProductDetails()
    MarketPlaceApi-->>UpdatesController: Product Catalog
    UpdatesController-->>MarketplaceUI: Listed Products

    User->>MarketplaceUI: Install Plugin/Theme
    MarketplaceUI->>UpdatesController: onInstallPackage()
    UpdatesController->>ExtensionSource: create(SOURCE_MARKET)
    ExtensionSource->>MarketPlaceApi: downloadPlugin/Theme()
    MarketPlaceApi->>FileSystem: Save Archive
    ExtensionSource->>FileSystem: Extract Archive
    ExtensionSource->>ExtensionManager: install()
    ExtensionManager->>FileSystem: Validate & Register
    ExtensionManager-->>UpdatesController: Installation Complete
    UpdatesController-->>MarketplaceUI: Status Update
Loading
sequenceDiagram
    participant ConsoleCmd as Console<br/>Command
    participant UpdateManager
    participant ModuleManager
    participant PluginManager
    participant ThemeManager

    ConsoleCmd->>UpdateManager: update(migrationsOnly: true)
    UpdateManager->>UpdateManager: check available updates
    alt Has Updates
        UpdateManager->>ModuleManager: update()
        ModuleManager->>ModuleManager: Run migrations, seed
        UpdateManager->>PluginManager: update()
        PluginManager->>PluginManager: Update, migrate, refresh
        UpdateManager->>ThemeManager: update()
        ThemeManager->>ThemeManager: Refresh themes
        UpdateManager-->>ConsoleCmd: Updates Complete
    else No Updates
        UpdateManager-->>ConsoleCmd: No Updates Available
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas requiring extra attention:

  • Extension manager inheritance hierarchy (ExtensionManager, PluginManager, ThemeManager, ModuleManager): Verify proper implementation of ExtensionManagerInterface methods and state management across all managers.
  • UpdateManager refactoring: Significant delegation changes from monolithic to manager-based approach; verify all migration paths and public API compatibility.
  • MarketPlaceApi integration: New external communication layer with HTTP signing and caching; verify security (API key handling), hash validation, and error handling.
  • Namespace migrations: Ensure all import changes are consistent; check for breaking changes in public aliases and old path support.
  • Plugin/Theme lifecycle methods: New enable(), disable(), uninstall(), refresh(), rollback() implementations across managers; verify idempotency and error recovery.
  • Console command reorganization: Verify that command registration and execution paths work correctly with new namespaces and dependencies.
  • Vue marketplace component: Verify integration with backend API, event handling, and error scenarios.
  • Database queries and migrations: Ensure Parameter storage, history tracking, and version management work correctly across new manager implementations.

🐰 With whiskers twitching with delight,
Extensions dance in namespace light,
The marketplace blooms, a garden so bright,
Where plugins and themes take flight!
🏪 ✨ 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.61% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "[WIP] Update Manager Refactor" directly references a significant component of the changeset, and the UpdateManager class does indeed undergo substantial refactoring. However, examining the raw_summary reveals that the scope of this PR extends well beyond UpdateManager changes alone. The PR introduces a unified extension management system architecture through new abstract classes (ExtensionManager), interfaces (ExtensionManagerInterface), and new manager implementations (ModuleManager), while simultaneously refactoring existing managers (PluginManager, ThemeManager). Additionally, a new MarketPlaceApi class is introduced, and there are substantial UI/controller overhaul changes. While the title correctly identifies UpdateManager as a key part of the refactoring, it does not fully convey that this is fundamentally an architectural refactoring of how extensions (modules, plugins, themes) are managed system-wide, which appears to be the primary objective according to the PR description itself: "Refactor of the UpdateManager and related systems to provide consistent interfaces for Winter extensions."
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch wip/support-uploading-packages-update-manager-changes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@LukeTowers
Copy link
Member

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Oct 29, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 58

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
modules/system/console/WinterUtil.php (1)

518-524: Add error handling and validate response structure for MarketPlaceApi call.

The MarketPlaceApi::request() method throws ApplicationException on API errors (non-200 responses, invalid JSON, etc.), but the code at line 518 has no try-catch to handle these exceptions. Additionally, the response structure is not validated—the code assumes $result['name'] and $result['owner'] exist without checking.

Wrap the API call in a try-catch block and validate the response structure:

try {
    $result = MarketPlaceApi::instance()->request(MarketPlaceApi::REQUEST_PROJECT_DETAIL, $projectId);
    
    if (empty($result['name']) || empty($result['owner'])) {
        $this->error("Invalid project response: missing required fields");
        return;
    }
    
    Parameter::set([
        'system::project.id'    => $projectId,
        'system::project.name'  => $result['name'],
        'system::project.owner' => $result['owner'],
    ]);
} catch (ApplicationException $e) {
    $this->error("Failed to fetch project details: " . $e->getMessage());
    return;
}
modules/system/console/plugin/PluginRollback.php (1)

29-33: Fix help text: "disable" → "rollback".

Minor UX clarity in the command signature.

Apply this diff:

-    protected $signature = 'plugin:rollback
-        {plugin : The plugin to disable. <info>(eg: Winter.Blog)</info>}
+    protected $signature = 'plugin:rollback
+        {plugin : The plugin to rollback. <info>(eg: Winter.Blog)</info>}
         {version? : If this parameter is not specified the plugin will be completely rolled back; otherwise it will stop on the specified version. <info>(eg: 1.3.9)</info>}
         {--f|force : Force the operation to run and ignore production warnings and confirmation questions.}';
modules/cms/classes/ThemeManager.php (1)

161-168: Null-check loaded theme before use in uninstall()

Theme::load() can return null; accessing isActiveTheme() would fatally error.

-        if (is_string($theme)) {
-            $theme = Theme::load($theme);
-        }
+        if (is_string($theme)) {
+            $loaded = Theme::load($theme);
+            if (!$loaded) {
+                throw new ApplicationException(sprintf('Theme "%s" not found.', $theme));
+            }
+            $theme = $loaded;
+        }
modules/system/controllers/Updates.php (3)

316-357: Broken call to removed UpdateManager::requestUpdateList()

This method no longer exists; the new API provides availableUpdates() and update(). This will fatal at runtime.

Suggested minimal interim change (WIP-safe): avoid server step building and delegate directly to update(), keeping UI flow:

-            $manager = UpdateManager::instance();
-            $result = $manager->requestUpdateList($force);
+            $manager = UpdateManager::instance();
+            // WIP: composer-only flow — build steps to run migrations only
+            $result = ['core' => null, 'plugins' => [], 'themes' => []];
...
-            $updateSteps = $this->buildUpdateSteps($core, $plugins, $themes, false);
+            $updateSteps = [];
...
             $updateSteps[] = [
                 'code' => 'completeUpdate',
                 'label' => Lang::get('system::lang.updates.update_completing'),
             ];

Longer-term: replace the legacy step engine with the new event-stream composer workflow hooked into the ExtensionManagers.


159-210: Legacy step executor targets removed methods

downloadCore/extractCore/downloadPlugin/extractPlugin/etc. were removed. Only completeUpdate/completeInstall should remain here in the short term.

         switch ($stepCode) {
-            case 'downloadCore':
-            ...
-            case 'extractPlugin':
-            ...
+            // Legacy steps removed in refactor; handled by ExtensionManagers now.
             case 'completeUpdate':
                 $manager->update();
                 Flash::success(Lang::get('system::lang.updates.update_success'));
                 return Redirect::refresh();
 
             case 'completeInstall':
                 $manager->update();
                 Flash::success(Lang::get('system::lang.install.install_success'));
                 return Redirect::refresh();
         }

Also consider guarding unknown codes with a warning to avoid silent no-ops.


362-449: Rework update step builder to align with ExtensionManagers

onApplyUpdates() + buildUpdateSteps() still build legacy server-driven steps. With composer-first updates, generate steps only for finalization (migrations) or replace with SSE-driven install flow.

I can draft a small adapter that translates Module/Plugin/ThemeManager::availableUpdates() into a flat steps list and executes via SSE; want me to open a follow-up PR?

Also applies to: 451-534

♻️ Duplicate comments (6)
composer.json (1)

33-33: Update winter/storm dependency before merge.

The dependency is currently pointing to a WIP branch, which is appropriate for development but must be updated to a stable reference before merging to production.

modules/system/controllers/updates/traits/ManagesMarketplaceProject.php (1)

9-9: ApplicationException import is already present.

The import for Winter\Storm\Exception\ApplicationException is already included at line 9, addressing the previous review comment.

modules/system/controllers/updates/assets/src/components/Product.vue (1)

47-51: License indicator (repeat).

Consider adding license info beside stars/downloads when available from the marketplace.

modules/system/classes/extensions/ExtensionManager.php (1)

56-86: Consider Winter’s Singleton trait (repeat).

Using the framework’s Singleton trait would standardize behavior and reduce boilerplate. If __construct constraints block it, document the rationale or adapt as discussed.

modules/system/controllers/updates/assets/src/components/MarketPlace.vue (1)

91-106: i18n placeholders.

Strings like “Sort”, “Per Page”, “Categories”, etc., should use translation keys (not urgent if planned).

modules/system/classes/extensions/ExtensionManagerInterface.php (1)

121-134: Name and guard “nuclear” operations.

tearDown() is destructive. Consider uninstallAll() plus an explicit confirmation mechanism (e.g., an environment flag or “I acknowledge” callback) to reduce accidental use.

🧹 Nitpick comments (44)
modules/system/controllers/updates/form.theme_upload.yaml (1)

1-6: Add a label and validation properties to improve form clarity.

This YAML form definition is missing common Winter CMS form field properties that would enhance the user experience. Specifically, the uploaded_package field lacks:

  • A label for human-readable display in the UI
  • A required property to enforce validation
  • Optionally, a comment property for helper text

Without these properties, the form input may appear unlabeled, making it unclear to users what file they should upload.

Consider applying this diff to add standard form field properties:

 fields:
     uploaded_package:
         type: fileupload
         span: full
         mode: file
         fileTypes: zip
+        label: winter.system::lang.theme_upload.uploaded_package
+        required: true
+        comment: winter.system::lang.theme_upload.uploaded_package_comment

Note: Adjust the label and comment keys to match your localization file structure.

modules/system/controllers/updates/assets/.eslintignore (1)

1-2: Consider using more robust patterns for compiled artifacts and clarify Vue file linting strategy.

The dist/*.js pattern only excludes JS files directly in dist/, not in nested subdirectories. If your build output includes nested directories, use dist/ (to exclude the entire directory) or dist/**/*.js. Additionally, the *.vue pattern excludes all Vue files from ESLint. Confirm whether this is intentional because you're using a separate Vue linter (e.g., eslint-plugin-vue), or if Vue files should be linted with ESLint using appropriate plugins.

If excluding the entire dist/ directory:

-dist/*.js
+dist/

If Vue files should be linted with ESLint plugins, remove the second line or configure eslint-plugin-vue in your .eslintrc instead of using the ignore file.

modules/system/controllers/updates/form.plugin_upload.yaml (1)

1-6: Add a label attribute for better UX and accessibility.

This form field lacks a label, which is a best practice for form inputs. Adding a label helps users understand what the field is for and improves accessibility.

Apply this diff to add a label:

  fields:
      uploaded_package:
          type: fileupload
+         label: Upload Plugin Package
          span: full
          mode: file
          fileTypes: zip

Additionally, consider adding validation constraints (e.g., required: true, maximum file size) and a help comment to guide users on acceptable file formats. Since the PR indicates localization strings are deferred, you may want to wrap the label text in a locale key if that pattern is used in this codebase:

label: system.update.plugin_upload_label
modules/system/console/BaseScaffoldCommand.php (1)

106-113: Consider differentiating colors for generated vs. updated files.

The rendering uses bg-yellow-600 for both "generated" and "updated" modes. Differentiating the colors could improve visual clarity (e.g., green for new files, blue/yellow for updates).

Example approach:

-        render(<<<HTML
-            <div class="mx-2">
-                <div class="px-1 bg-yellow-600">File $mode</div>
-                <em class="ml-1">
-                    $relativeFile
-                </em>
-            </div>
-        HTML);
+        $color = $mode === 'generated' ? 'bg-green-600' : 'bg-blue-600';
+        render(<<<HTML
+            <div class="mx-2">
+                <div class="px-1 $color">File $mode</div>
+                <em class="ml-1">
+                    $relativeFile
+                </em>
+            </div>
+        HTML);
modules/system/classes/core/package-list.json (1)

1-1858: Test data should be temporary.

Per PR objectives, this is demo data meant to emulate the Winter Marketplace API for experimental testing. Please ensure:

  1. This file is removed before final merge, or
  2. Clearly documented as fixture data if intentionally kept
  3. Future timestamps (2025-05-14, 2025-07-27) and empty URL fields are addressed for production use

Consider moving this to a dedicated test fixtures directory (e.g., tests/fixtures/) if it needs to be retained for testing purposes.

modules/system/controllers/updates/assets/src/utils/numbers.js (1)

1-36: Consider explicit NaN handling.

Returning 0 for NaN values (line 6) may mask invalid inputs. Consider whether returning an empty string, null, or throwing an error would be more appropriate for your use case.

Additionally, the decimal padding logic on lines 32-34 could be slightly more readable:

-        s[1] += new Array(decimals - s[1].length + 1).join('0');
+        s[1] += Array(decimals - s[1].length + 1).fill('0').join('');
modules/system/classes/extensions/Preserver.php (1)

29-52: Simplify sprintf format and verify archive file extension.

The sprintf format on lines 39-45 is complex with positional argument %4$s. Consider simplifying for better readability.

Additionally, verify that the packArchive method (line 49) adds the .zip extension to the archive filename, as it's not explicitly included in the path passed to Storage::path().

Simplified alternative:

-        $extensionArchiveDir = sprintf(
-            '%s%4$s%s%4$s%s',
-            static::ROOT_PATH,
-            $type,
-            $extension->getIdentifier(),
-            DIRECTORY_SEPARATOR
-        );
+        $extensionArchiveDir = implode(DIRECTORY_SEPARATOR, [
+            static::ROOT_PATH,
+            $type,
+            $extension->getIdentifier()
+        ]);
modules/system/classes/extensions/traits/HasVersionFile.php (1)

38-46: Optional: Decouple script filtering from $this->getPath() by deriving from version.yaml location.

Using $this->getPath().'/updates/' assumes a specific structure. Deriving from dirname($versionFile) makes the trait more reusable.

Example:

-                $details = array_values(array_filter($details, function ($string) {
-                    return !Str::endsWith($string, '.php') || !File::exists($this->getPath() . '/updates/' . $string);
+                $details = array_values(array_filter($details, function ($string) use ($versionFile) {
+                    if (!Str::endsWith($string, '.php')) return true;
+                    $updatesDir = dirname($versionFile);
+                    return !File::exists($updatesDir . DIRECTORY_SEPARATOR . $string);
                 }));
modules/system/console/plugin/PluginRollback.php (1)

40-42: Update docblock to reference PluginManager, not UpdateManager.

Reflects the new implementation.

-     * @throws \Exception if the UpdateManager is unable to rollback the requested plugin to the requested version
+     * @throws \Exception if the PluginManager is unable to rollback the requested plugin to the requested version
modules/system/controllers/updates/assets/src/utils/message.js (1)

33-43: Code duplication: Same regex used twice.

Lines 33 and 40 use the identical regex pattern /\.*?[\d][\w]*DONE$/. This is unnecessarily duplicated.

Reuse the matched result:

     search = line.match(/\.*?[\d][\w]*DONE$/);

     let ending = null;
     if (search) {
         line = `<div class="pl-3">${line.substring(0, line.length - search[0].length)}</div>`;
         ending = `
             <span class="text-orange-500 ml-auto">
-                ${search[0].match(/\.*?[\d][\w]*DONE$/)[0].replace(/\.*/, '').replace('DONE', '')}
+                ${search[0].replace(/\.*/, '').replace('DONE', '')}
             </span>
         `;
     }
modules/system/controllers/updates/assets/src/updates.js (1)

23-33: Fix typo in comment.

Line 25 has a typo: "vuw" should be "vue".

-            // Add request to vuw
+            // Add request to vue
             app.request = (handler, options) => Snowboard.request(handler, options);
modules/system/controllers/updates/index.php (1)

229-233: Inline style override suggests layout integration issue.

The inline style overriding #layout-body padding suggests the new UI doesn't integrate cleanly with the existing layout system. This should ideally be handled through proper CSS architecture rather than inline overrides.

Consider:

  1. Adding a proper CSS class to the layout configuration
  2. Creating a dedicated stylesheet for the updates view
  3. Using a layout configuration option to control padding
modules/system/controllers/updates/assets/src/components/Product.vue (2)

95-100: Avoid window in template; compute a safe details URL.

Bind a computed detailsHref to avoid direct window access in template, and handle missing /install gracefully.

-                <a v-if="product.installed"
-                   :href="window.location.href.replace('/install', `/details/${product.code.toLowerCase().replace('.', '-')}`)"
+                <a v-if="product.installed"
+                   :href="detailsHref"
                    class="btn btn-outline-info">

Add:

@@
 export default {
   props: ['product', 'type'],
   data: () => ({
@@
   computed: {
+    detailsHref() {
+      try {
+        const url = new URL(window.location.href);
+        url.pathname = url.pathname.replace(/\/install(?:\/)?$/, `/details/${this.product.code.toLowerCase().replace('.', '-')}`);
+        return url.toString();
+      } catch {
+        return '#';
+      }
+    },
     window: {

11-23: Tailwind utility validity.

Classes like size-38 and group-hover:w-50 may not exist unless configured; prefer arbitrary values: size-[9.5rem], w-[12.5rem].

modules/system/classes/extensions/ExtensionManager.php (1)

5-13: Remove unused imports.

Component and Termwind aren’t used here.

-use Illuminate\Console\View\Components\Component;
@@
-use Termwind\Termwind;
modules/system/console/plugin/PluginInstall.php (4)

112-113: Remove unused variable.

$pluginsToMigrate is assigned but never used.

-                    $pluginsToMigrate[] = $code;

83-96: Clean replace install path to avoid stale files.

When replacing an installed plugin, delete the existing directory before copying to prevent leftover files.

-                // @TODO: Check if plugin is already installed
                 if ($pluginManager->findByIdentifier($code)) {
                     if (!$this->confirm(sprintf(
                         'Plugin %s is already installed. Would you like to replace the currently installed version with the one found in %s?',
                         $code,
                         $pluginName
                     ))) {
                         $this->output->writeln(sprintf('<info>Skipping plugin: %s</info>', $code));
                         unset($plugins[$code]);
                         continue;
                     }
                 }
 
                 $this->output->writeln(sprintf('<info>Installing plugin: %s</info>', $code));
                 $pluginDir = plugins_path(strtolower(str_replace('.', DIRECTORY_SEPARATOR, $code)));
 
                 try {
+                    if (File::isDirectory($pluginDir)) {
+                        File::deleteDirectory($pluginDir);
+                    }
                     // Copy the files
                     File::copyDirectory(pathinfo($pluginClassFile, PATHINFO_DIRNAME), $pluginDir);

Also applies to: 100-126


151-154: Fix sprintf usage in message.

Argument provided but not used; include %s or remove the arg.

-        $this->output->writeln(sprintf('<info>Migrating plugin...</info>', $code));
+        $this->output->writeln(sprintf('<info>Migrating plugin: %s</info>', $code));

66-71: Zip extraction safety.

Ensure extractArchive protects against zip-slip (path traversal) and malicious symlinks within archives.

modules/system/controllers/updates/assets/src/components/MarketPlace.vue (5)

286-296: Don’t mutate source when sorting.

Clone before sort to keep this.products stable.

-            return products.sort({
+            return [...products].sort({
                 new: (a, b) => (new Date(b.updated_at)).getTime() - (new Date(a.updated_at)).getTime(),
                 downloads: (a, b) => b.downloads - a.downloads,
                 stars: (a, b) => b.stars - a.stars,
             }[this.sort]);

109-111: Add keys to v-for for stable rendering.

Provide unique :key for products.

-                    <Product v-for="product in pageProducts" :product="product" :type="this.mode"></Product>
+                    <Product v-for="product in pageProducts" :key="product.code" :product="product" :type="mode"></Product>

19-31: Add keys to other v-for loops.

Keys improve diffing and performance.

-                    <li v-for="type in ['all', 'free', 'paid']">
+                    <li v-for="type in ['all', 'free', 'paid']" :key="type">
...
-                    <li v-for="feature in [{label: 'Translate Support', key: 'translate', icon: 'icon-language'}]" class="h-full">
+                    <li v-for="feature in [{label: 'Translate Support', key: 'translate', icon: 'icon-language'}]" :key="feature.key" class="h-full">
...
-                    <li v-for="category in categories" class="mb-1">
+                    <li v-for="category in categories" :key="category.id" class="mb-1">

Also applies to: 34-50, 57-66


160-166: Case-insensitive search and null guards.

Make includes checks safer.

-                        results = results.filter((product) => {
-                            return product.name.includes(this.search)
-                                || product.description.includes(this.search)
-                                || product.composer_package.includes(this.search);
-                        });
+                        const q = this.search.toLowerCase();
+                        results = results.filter((p) =>
+                            (p.name || '').toLowerCase().includes(q) ||
+                            (p.description || '').toLowerCase().includes(q) ||
+                            (p.composer_package || '').toLowerCase().includes(q)
+                        );

228-235: Remove filterPrice toggle hack; initialize cleanly.

Set default to 'all' and drop the forced toggling.

-                // ... vue really doesn't like `all` being the default ...
-                this.filterPrice = 'all';
-                this.filterPrice = null;
-                this.$nextTick(() => {
-                    this.filterPrice = 'all';
-                });
+                this.filterPrice = 'all';
modules/system/classes/extensions/ExtensionManagerInterface.php (3)

35-35: Avoid reserved-name method list().

Using list() risks confusion with PHP’s language construct. Prefer all(), listInstalled(), or getAll().


72-86: Narrow the enable/disable flag type.

string|bool $flag is ambiguous. Prefer a dedicated enum (PHP 8.1+) or ?string with documented string-literals (DISABLED_*). Improves static analysis and call-site clarity.


94-110: Return types are too loose.

mixed on update/refresh/rollback hinders consumers. Consider concrete returns (bool|array|WinterExtension|null) per method contract and update docs accordingly.

modules/system/controllers/updates/traits/ManagesPlugins.php (1)

250-267: Ensure cached streaming output uses a TTL.

doWrite() appends to a cache entry without expiry. Add a TTL to each write to keep the key alive for a bounded time window (e.g., 15 minutes).

Apply this diff:

- Cache::put($this->key, Cache::get($this->key, '') . trim($message) . ($newline ? "\n" : ''));
+ Cache::put(
+     $this->key,
+     Cache::get($this->key, '') . trim($message) . ($newline ? "\n" : ''),
+     now()->addMinutes(15)
+ );
modules/system/classes/extensions/PluginVersionManager.php (1)

498-506: Optional: add existence check before packDown for symmetry.

You check for file existence in applyDatabaseScript but not in removeDatabaseScript. Consider mirroring the guard to avoid noisy failures when files are missing.

-        $this->updater->packDown($updateFile);
+        if (File::isFile($updateFile)) {
+            $this->updater->packDown($updateFile);
+        } else {
+            $this->pluginManager->renderComponent(Error::class, sprintf('Migration file "%s" not found.', $script));
+        }

Also applies to: 517-526

modules/system/aliases.php (1)

94-98: Alias targets verified and present; deprecation notes are optional.

All scaffold and extension manager alias targets have been confirmed to exist and are correctly namespaced:

  • System\Console\Create\CreateCommand, CreateModel, CreatePlugin, CreateSettings
  • System\Classes\Extensions\PluginManager, PluginBase

The backward compatibility mappings are sound. Adding deprecation notes in CHANGELOG/UPGRADE is optional but recommended to guide plugin authors toward the canonical System namespace.

modules/system/classes/extensions/ModuleManager.php (1)

36-43: Guard output usage to avoid null dereference

init()/setOutput() call $this->migrator->setOutput($this->output) before $this->output may be set. Add a null check.

-        $this->migrator->setOutput($this->output);
+        if (isset($this->output)) {
+            $this->migrator->setOutput($this->output);
+        }

And similarly where writing to $this->output when $firstUp.

Also applies to: 91-97

modules/cms/classes/ThemeManager.php (1)

49-63: Doc return type should allow null

findByDirName() can return null; update docblock to reflect nullable return.

-     * @return string
+     * @return string|null
modules/system/classes/extensions/PluginBase.php (1)

472-495: Return early on first existing markdown file

For array input, you currently overwrite $contents and return the last found file. Prefer first match.

-        foreach ($filenames as $file) {
+        foreach ($filenames as $file) {
             if (!File::exists($path . '/' . $file)) {
                 continue;
             }
 
             $contents = File::get($path . '/' . $file);
 
             // Parse markdown, clean HTML, remove first H1 tag
             $contents = Markdown::parse($contents);
             $contents = Html::clean($contents);
             $contents = preg_replace('@<h1[^>]*?>.*?<\/h1>@si', '', $contents, 1);
+            break;
         }
modules/system/controllers/Updates.php (3)

227-245: hasImportantUpdates logic needs revisiting

Counting modules to set "important" no longer matches the old "!!!" semantics. Either drop the flag or compute importance from metadata when available.


106-113: Remove dead asset include comment

Leftover commented addJs; remove or restore Vite alternative if needed.


73-74: Surface warnings earlier

Good call pulling warnings from PluginManager. Consider also surfacing cms.disableCoreUpdates here for UX consistency with ModuleManager.

modules/system/classes/UpdateManager.php (3)

87-95: Normalize nulls from managers

Ensure availableUpdates() always returns arrays to avoid count()/array_reduce warnings.

-        return [
-            'modules' => ModuleManager::instance()->availableUpdates(),
-            'plugins' => PluginManager::instance()->availableUpdates(),
-            'themes' => ThemeManager::instance()->availableUpdates(),
-        ];
+        return [
+            'modules' => ModuleManager::instance()->availableUpdates() ?? [],
+            'plugins' => PluginManager::instance()->availableUpdates() ?? [],
+            'themes' => ThemeManager::instance()->availableUpdates() ?? [],
+        ];

28-39: Consider honoring disableCoreUpdates

You store disableCoreUpdates but never use it; either remove the property or use it to skip core-related checks/updates, and surface a warning (consistent with ModuleManager).

Also applies to: 96-107


56-85: Retry logic refinement in check()

Current short-circuit only when previous count > 0; you might want to respect the retry window regardless to throttle remote checks, and cache zeroes too.

-            && $updateCount > 0

Remove this guard or gate by a separate "lastChecked" timestamp.

modules/system/classes/core/MarketPlaceApi.php (2)

183-195: Redundant JSON validity checks after JSON_THROW_ON_ERROR.

With JSON_THROW_ON_ERROR, json_decode won’t return false. Reduce to a single is_array guard.

         try {
             $resultData = json_decode($result->body, true, flags: JSON_THROW_ON_ERROR);
         } catch (Exception $ex) {
             throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
         }
-
-        if ($resultData === false || (is_string($resultData) && !strlen($resultData))) {
-            throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
-        }
-
-        if (!is_array($resultData)) {
+        if (!is_array($resultData)) {
             throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
         }

507-512: Low‑signal error message from getPackagesFromBazaar().

Replace “Bad things have happened” with a translated, actionable message.

-        if ($result->code !== 200) {
-            throw new ApplicationException('Bad things have happened');
-        }
+        if ($result->code !== 200) {
+            throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
+        }
modules/system/classes/extensions/PluginManager.php (3)

172-186: “install” path calls updatePlugin(); confirm intended install behavior.

This treats install as an update of migrations only. If files must be fetched (composer/market), ensure that happens here before migrations.


353-369: Unused $code variable in foreach (PHPMD).

Remove the unused key variable to silence the warning and clarify intent.

-        foreach ($this->list() as $code => $plugin) {
+        foreach ($this->list() as $plugin) {

1493-1506: resolveIdentifier(): scope is fine; optional strengthening.

Looks good. Optionally tighten accepted types in PHPDoc to reflect allowed WinterExtension implementations.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cd054e and f706326.

⛔ Files ignored due to path filters (9)
  • modules/system/assets/dist/assets/updates-8hFxTJT2.css is excluded by !**/dist/**
  • modules/system/assets/dist/assets/updates-BHH8rdGj.css is excluded by !**/dist/**
  • modules/system/assets/dist/assets/updates-CRrv2aGu.js is excluded by !**/dist/**
  • modules/system/assets/dist/manifest.json is excluded by !**/dist/**
  • modules/system/tests/fixtures/plugin-archives/entire-plugins-directory.zip is excluded by !**/*.zip
  • modules/system/tests/fixtures/plugin-archives/multiple-authors.zip is excluded by !**/*.zip
  • modules/system/tests/fixtures/plugin-archives/multiple-plugins-no-author.zip is excluded by !**/*.zip
  • modules/system/tests/fixtures/plugin-archives/single-plugin-contents.zip is excluded by !**/*.zip
  • modules/system/tests/fixtures/plugin-archives/single-plugin-nested.zip is excluded by !**/*.zip
📒 Files selected for processing (107)
  • composer.json (2 hunks)
  • modules/backend/ServiceProvider.php (1 hunks)
  • modules/backend/classes/AuthManager.php (1 hunks)
  • modules/backend/classes/BackendController.php (1 hunks)
  • modules/backend/classes/NavigationManager.php (1 hunks)
  • modules/backend/classes/WidgetManager.php (1 hunks)
  • modules/backend/widgets/Lists.php (1 hunks)
  • modules/cms/ServiceProvider.php (5 hunks)
  • modules/cms/classes/ComponentManager.php (1 hunks)
  • modules/cms/classes/Theme.php (6 hunks)
  • modules/cms/classes/ThemeManager.php (3 hunks)
  • modules/cms/console/ThemeInstall.php (2 hunks)
  • modules/cms/widgets/ComponentList.php (1 hunks)
  • modules/system/ServiceProvider.php (8 hunks)
  • modules/system/aliases.php (1 hunks)
  • modules/system/classes/FileManifest.php (2 hunks)
  • modules/system/classes/MailManager.php (1 hunks)
  • modules/system/classes/MarkupManager.php (2 hunks)
  • modules/system/classes/SettingsManager.php (1 hunks)
  • modules/system/classes/SourceManifest.php (2 hunks)
  • modules/system/classes/UpdateManager.php (5 hunks)
  • modules/system/classes/asset/PackageManager.php (1 hunks)
  • modules/system/classes/core/MarketPlaceApi.php (1 hunks)
  • modules/system/classes/core/package-list.json (1 hunks)
  • modules/system/classes/extensions/ExtensionManager.php (1 hunks)
  • modules/system/classes/extensions/ExtensionManagerInterface.php (1 hunks)
  • modules/system/classes/extensions/ModuleManager.php (1 hunks)
  • modules/system/classes/extensions/PluginBase.php (8 hunks)
  • modules/system/classes/extensions/PluginManager.php (9 hunks)
  • modules/system/classes/extensions/PluginVersionManager.php (17 hunks)
  • modules/system/classes/extensions/Preserver.php (1 hunks)
  • modules/system/classes/extensions/source/ComposerSource.php (1 hunks)
  • modules/system/classes/extensions/source/ExtensionSource.php (1 hunks)
  • modules/system/classes/extensions/source/LocalSource.php (1 hunks)
  • modules/system/classes/extensions/source/MarketSource.php (1 hunks)
  • modules/system/classes/extensions/traits/HasVersionFile.php (1 hunks)
  • modules/system/console/BaseScaffoldCommand.php (2 hunks)
  • modules/system/console/PluginInstall.php (0 hunks)
  • modules/system/console/WinterDown.php (3 hunks)
  • modules/system/console/WinterInstall.php (0 hunks)
  • modules/system/console/WinterTest.php (1 hunks)
  • modules/system/console/WinterUp.php (2 hunks)
  • modules/system/console/WinterUpdate.php (1 hunks)
  • modules/system/console/WinterUtil.php (2 hunks)
  • modules/system/console/asset/AssetCreate.php (2 hunks)
  • modules/system/console/asset/AssetInstall.php (1 hunks)
  • modules/system/console/asset/fixtures/config/tailwind/tailwind.module.config.js.fixture (1 hunks)
  • modules/system/console/asset/npm/NpmVersion.php (1 hunks)
  • modules/system/console/create/CreateCommand.php (1 hunks)
  • modules/system/console/create/CreateFactory.php (1 hunks)
  • modules/system/console/create/CreateJob.php (1 hunks)
  • modules/system/console/create/CreateMigration.php (2 hunks)
  • modules/system/console/create/CreateModel.php (1 hunks)
  • modules/system/console/create/CreatePlugin.php (1 hunks)
  • modules/system/console/create/CreateSettings.php (1 hunks)
  • modules/system/console/create/CreateTest.php (1 hunks)
  • modules/system/console/create/scaffold/plugin/plugin.stub (1 hunks)
  • modules/system/console/plugin/PluginDisable.php (2 hunks)
  • modules/system/console/plugin/PluginEnable.php (2 hunks)
  • modules/system/console/plugin/PluginInstall.php (1 hunks)
  • modules/system/console/plugin/PluginList.php (3 hunks)
  • modules/system/console/plugin/PluginRefresh.php (2 hunks)
  • modules/system/console/plugin/PluginRemove.php (3 hunks)
  • modules/system/console/plugin/PluginRollback.php (4 hunks)
  • modules/system/console/traits/HasPluginArgument.php (1 hunks)
  • modules/system/controllers/Updates.php (20 hunks)
  • modules/system/controllers/updates/_install_plugins.php (1 hunks)
  • modules/system/controllers/updates/_install_themes.php (1 hunks)
  • modules/system/controllers/updates/_is_enabled.php (0 hunks)
  • modules/system/controllers/updates/_is_unfrozen.php (0 hunks)
  • modules/system/controllers/updates/_list_manage_toolbar.php (0 hunks)
  • modules/system/controllers/updates/_list_toolbar.php (0 hunks)
  • modules/system/controllers/updates/_name.php (1 hunks)
  • modules/system/controllers/updates/_open_link.php (1 hunks)
  • modules/system/controllers/updates/_popup_upload_plugin.php (1 hunks)
  • modules/system/controllers/updates/_update_list.php (1 hunks)
  • modules/system/controllers/updates/_version.php (1 hunks)
  • modules/system/controllers/updates/assets/.eslintignore (1 hunks)
  • modules/system/controllers/updates/assets/src/components/MarketPlace.vue (1 hunks)
  • modules/system/controllers/updates/assets/src/components/Pagination.vue (1 hunks)
  • modules/system/controllers/updates/assets/src/components/Product.vue (1 hunks)
  • modules/system/controllers/updates/assets/src/tailwind.config.js (1 hunks)
  • modules/system/controllers/updates/assets/src/updates.css (1 hunks)
  • modules/system/controllers/updates/assets/src/updates.js (1 hunks)
  • modules/system/controllers/updates/assets/src/utils/message.js (1 hunks)
  • modules/system/controllers/updates/assets/src/utils/numbers.js (1 hunks)
  • modules/system/controllers/updates/assets/src/utils/winter-request.js (1 hunks)
  • modules/system/controllers/updates/config_list.yaml (1 hunks)
  • modules/system/controllers/updates/config_manage_list.yaml (0 hunks)
  • modules/system/controllers/updates/form.plugin_upload.yaml (1 hunks)
  • modules/system/controllers/updates/form.theme_upload.yaml (1 hunks)
  • modules/system/controllers/updates/index.php (1 hunks)
  • modules/system/controllers/updates/install.php (1 hunks)
  • modules/system/controllers/updates/manage.php (0 hunks)
  • modules/system/controllers/updates/traits/ManagesMarketplaceProject.php (1 hunks)
  • modules/system/controllers/updates/traits/ManagesPlugins.php (1 hunks)
  • modules/system/controllers/updates/traits/ManagesThemes.php (1 hunks)
  • modules/system/lang/en/lang.php (3 hunks)
  • modules/system/models/MailSetting.php (2 hunks)
  • modules/system/models/Parameter.php (1 hunks)
  • modules/system/models/PluginVersion.php (2 hunks)
  • modules/system/models/pluginversion/columns.yaml (2 hunks)
  • modules/system/models/pluginversion/columns_manage.yaml (0 hunks)
  • modules/system/package.json (2 hunks)
  • modules/system/postcss.config.mjs (1 hunks)
  • modules/system/reportwidgets/Status.php (1 hunks)
  • modules/system/tests/README.md (1 hunks)
⛔ Files not processed due to max files limit (33)
  • modules/system/tests/bootstrap/PluginManagerTestCase.php
  • modules/system/tests/bootstrap/PluginTestCase.php
  • modules/system/tests/classes/CoreLangTest.php
  • modules/system/tests/classes/PluginManagerTest.php
  • modules/system/tests/classes/UpdateManagerTest.php
  • modules/system/tests/classes/VersionManagerTest.php
  • modules/system/tests/console/asset/mix/MixCreateTest.php
  • modules/system/tests/console/asset/vite/BaseViteTestCase.php
  • modules/system/tests/console/asset/vite/ViteCompileTest.php
  • modules/system/tests/console/asset/vite/ViteCreateTest.php
  • modules/system/tests/fixtures/plugins/database/tester/Plugin.php
  • modules/system/tests/fixtures/plugins/dependencytest/acme/Plugin.php
  • modules/system/tests/fixtures/plugins/dependencytest/dependency/Plugin.php
  • modules/system/tests/fixtures/plugins/dependencytest/found/Plugin.php
  • modules/system/tests/fixtures/plugins/dependencytest/notfound/Plugin.php
  • modules/system/tests/fixtures/plugins/dependencytest/wrongcase/Plugin.php
  • modules/system/tests/fixtures/plugins/mix/testa/Plugin.php
  • modules/system/tests/fixtures/plugins/mix/testb/Plugin.php
  • modules/system/tests/fixtures/plugins/mix/testc/Plugin.php
  • modules/system/tests/fixtures/plugins/testvendor/goto/Plugin.php
  • modules/system/tests/fixtures/plugins/testvendor/test/Plugin.php
  • modules/system/tests/fixtures/plugins/winter/invalidreplacement/Plugin.php
  • modules/system/tests/fixtures/plugins/winter/noupdates/Plugin.php
  • modules/system/tests/fixtures/plugins/winter/original/Plugin.php
  • modules/system/tests/fixtures/plugins/winter/replacement/Plugin.php
  • modules/system/tests/fixtures/plugins/winter/replacenotinstalled/Plugin.php
  • modules/system/tests/fixtures/plugins/winter/sample/Plugin.php
  • modules/system/tests/fixtures/plugins/winter/tester/Plugin.php
  • modules/system/traits/AssetMaker.php
  • modules/system/traits/InteractsWithZip.php
  • modules/system/vite.config.mjs
  • modules/system/winter.mix.js
  • plugins/winter/demo/Plugin.php
💤 Files with no reviewable changes (9)
  • modules/system/controllers/updates/_is_enabled.php
  • modules/system/console/WinterInstall.php
  • modules/system/controllers/updates/_list_toolbar.php
  • modules/system/console/PluginInstall.php
  • modules/system/controllers/updates/_list_manage_toolbar.php
  • modules/system/models/pluginversion/columns_manage.yaml
  • modules/system/controllers/updates/_is_unfrozen.php
  • modules/system/controllers/updates/config_manage_list.yaml
  • modules/system/controllers/updates/manage.php
🧰 Additional context used
🪛 Biome (2.1.2)
modules/system/controllers/updates/assets/src/updates.css

[error] 47-47: expected } but instead the file ends

the file ends here

(parse)

🪛 GitHub Actions: Tests
modules/system/classes/extensions/PluginBase.php

[error] 27-27: While running 'php artisan package:discover', PHP Fatal error: Trait 'Winter\Storm\Support\Traits\HasComposerPackage' not found in PluginBase.php (line 27).

🪛 PHPMD (2.15.0)
modules/system/console/plugin/PluginInstall.php

112-112: Avoid unused local variables such as '$pluginsToMigrate'. (undefined)

(UnusedLocalVariable)

modules/system/classes/core/MarketPlaceApi.php

303-303: Avoid unused parameters such as '$postData'. (undefined)

(UnusedFormalParameter)

modules/system/controllers/updates/traits/ManagesPlugins.php

45-45: Avoid unused local variables such as '$name'. (undefined)

(UnusedLocalVariable)


194-194: Avoid unused local variables such as '$manager'. (undefined)

(UnusedLocalVariable)

modules/cms/classes/ThemeManager.php

123-123: Avoid unused parameters such as '$extension'. (undefined)

(UnusedFormalParameter)


123-123: Avoid unused parameters such as '$flag'. (undefined)

(UnusedFormalParameter)


128-128: Avoid unused parameters such as '$extension'. (undefined)

(UnusedFormalParameter)


128-128: Avoid unused parameters such as '$flag'. (undefined)

(UnusedFormalParameter)


133-133: Avoid unused parameters such as '$extension'. (undefined)

(UnusedFormalParameter)


133-133: Avoid unused parameters such as '$migrationsOnly'. (undefined)

(UnusedFormalParameter)


139-139: Avoid unused parameters such as '$extension'. (undefined)

(UnusedFormalParameter)


144-144: Avoid unused parameters such as '$extension'. (undefined)

(UnusedFormalParameter)


144-144: Avoid unused parameters such as '$targetVersion'. (undefined)

(UnusedFormalParameter)


155-155: Avoid unused parameters such as '$noRollback'. (undefined)

(UnusedFormalParameter)

modules/system/classes/extensions/ModuleManager.php

53-53: Avoid unused parameters such as '$extension'. (undefined)

(UnusedFormalParameter)


72-72: Avoid unused parameters such as '$extension'. (undefined)

(UnusedFormalParameter)


72-72: Avoid unused parameters such as '$flag'. (undefined)

(UnusedFormalParameter)


78-78: Avoid unused parameters such as '$extension'. (undefined)

(UnusedFormalParameter)


78-78: Avoid unused parameters such as '$flag'. (undefined)

(UnusedFormalParameter)


168-168: Avoid unused parameters such as '$targetVersion'. (undefined)

(UnusedFormalParameter)

modules/system/classes/extensions/PluginManager.php

355-355: Avoid unused local variables such as '$code'. (undefined)

(UnusedLocalVariable)

Comment on lines +728 to +743
public function getVersion(): string
{
if (isset($this->version)) {
return $this->version;
}

$versions = $this->getVersionsFromYaml(
versionFile: $this->getPath() . DIRECTORY_SEPARATOR . 'version.yaml',
includeScripts: false
);
if (empty($versions)) {
return $this->version = '0';
}

return $this->version = trim(key(array_slice($versions, -1, 1)));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

getVersion() returns "0"/wrong key due to array_slice reindexing; use array_key_last instead.

array_slice reindexes keys, so key(array_slice(...)) yields 0. Return the actual last version key.

Apply this diff:

     public function getVersion(): string
     {
         if (isset($this->version)) {
             return $this->version;
         }

         $versions = $this->getVersionsFromYaml(
             versionFile: $this->getPath() . DIRECTORY_SEPARATOR . 'version.yaml',
             includeScripts: false
         );
         if (empty($versions)) {
             return $this->version = '0';
         }

-        return $this->version = trim(key(array_slice($versions, -1, 1)));
+        $last = array_key_last($versions);
+        return $this->version = is_string($last) ? trim($last) : '0';
     }
🤖 Prompt for AI Agents
In modules/cms/classes/Theme.php around lines 728 to 743, the current code uses
key(array_slice($versions, -1, 1)) which reindexes the array and returns 0
instead of the actual last version key; replace that logic with array_key_last
to get the real last key, assign it to $this->version and return its trimmed
string value (fall back to '0' if array_key_last returns null/false). Make the
change so you compute $lastKey = array_key_last($versions); if ($lastKey ===
null || $lastKey === false) return $this->version = '0'; otherwise return
$this->version = trim((string)$lastKey).

Comment on lines +666 to +676
/**
* Flushes local cache
*
* @return $this
*/
public function forgetCache(): static
{
// unset($this->databaseHistory, $this->databaseVersions, $this->fileVersions);

return $this;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

forgetCache() currently does nothing.

It should clear $databaseHistory, $databaseVersions, and $fileVersions; otherwise callers will get stale data.

Apply this diff:

     public function forgetCache(): static
     {
-//        unset($this->databaseHistory, $this->databaseVersions, $this->fileVersions);
+        unset($this->databaseHistory, $this->databaseVersions, $this->fileVersions);
         return $this;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Flushes local cache
*
* @return $this
*/
public function forgetCache(): static
{
// unset($this->databaseHistory, $this->databaseVersions, $this->fileVersions);
return $this;
}
/**
* Flushes local cache
*
* @return $this
*/
public function forgetCache(): static
{
unset($this->databaseHistory, $this->databaseVersions, $this->fileVersions);
return $this;
}
🤖 Prompt for AI Agents
In modules/system/classes/extensions/PluginVersionManager.php around lines 666
to 676, forgetCache() currently returns $this without clearing any cached state;
restore clearing of the cache by resetting $this->databaseHistory,
$this->databaseVersions, and $this->fileVersions (e.g., unset them or set them
to null/empty arrays as appropriate for the class) before returning $this so
callers no longer receive stale data.

Comment on lines +1 to +24
<?php

namespace System\Classes\Extensions;

use Cms\Classes\Theme;
use Illuminate\Support\Facades\Storage;
use System\Traits\InteractsWithZip;
use Winter\Storm\Exception\ApplicationException;
use Winter\Storm\Foundation\Extension\WinterExtension;
use Winter\Storm\Support\ModuleServiceProvider;
use Winter\Storm\Support\Traits\Singleton;

class Preserver
{
use Singleton;
use InteractsWithZip;

public const ROOT_PATH = 'archive';

protected array $classMap = [
ModuleServiceProvider::class => 'modules',
PluginBase::class => 'plugins',
Theme::class => 'themes',
];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing import for PluginBase class.

Line 22 references PluginBase::class in the $classMap array, but there's no corresponding import statement at the top of the file.

Add the missing import:

 use Cms\Classes\Theme;
 use Illuminate\Support\Facades\Storage;
+use System\Classes\Extensions\PluginBase;
 use System\Traits\InteractsWithZip;
🤖 Prompt for AI Agents
In modules/system/classes/extensions/Preserver.php around lines 1 to 24, the
$classMap references PluginBase::class but PluginBase is not imported; add the
missing import statement "use System\Classes\PluginBase;" alongside the other
use statements at the top of the file (grouped/sorted with the existing imports)
so the class reference resolves correctly.

Comment on lines +16 to +17
use Winter\Packager\Exceptions\CommandException;
use Winter\Storm\Exception\ApplicationException;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Wrong CommandException namespace — composer errors won’t be caught.

Composer is from Winter\Storm\Packager\Composer but you catch Winter\Packager\Exceptions\CommandException. This will miss actual exceptions.

Apply this diff to fix both catch sites:

- use Winter\Packager\Exceptions\CommandException;
+ use Winter\Storm\Packager\Exceptions\CommandException;

Also applies to: 120-129, 232-237

🤖 Prompt for AI Agents
In modules/system/classes/extensions/source/ExtensionSource.php around lines
16-17, 120-129, and 232-237 the code imports and catches
Winter\Packager\Exceptions\CommandException which is the wrong namespace for
Composer-related errors; update the import to
Winter\Storm\Packager\Exceptions\CommandException (or fully-qualify that correct
namespace where used) and update the corresponding catch clauses so they catch
the corrected CommandException type (ensuring any existing catch order remains
correct and Composer errors will be caught).

Comment on lines +28 to +42
public static function fromZip(string $path): array
{
if (!File::exists($path)) {
throw new ApplicationException("$path doesn't exist");
}

$dir = temp_path(time());

if (!File::exists($dir)) {
File::makeDirectory($dir);
}

(new class {
use InteractsWithZip;
})->extractArchive($path, $dir);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential directory name collision and path exposure.

Line 34 uses time() to create a temporary directory name, which can collide if fromZip is called multiple times within the same second.

Line 31 exposes the server path in the error message, which could be a minor information disclosure issue.

-        if (!File::exists($path)) {
-            throw new ApplicationException("$path doesn't exist");
+        if (!File::exists($path)) {
+            throw new ApplicationException("Provided file path doesn't exist");
         }

-        $dir = temp_path(time());
+        $dir = temp_path(uniqid('extension_', true));
🤖 Prompt for AI Agents
In modules/system/classes/extensions/source/LocalSource.php around lines 28 to
42, the current implementation uses time() to generate a temporary directory
(risking collisions if called multiple times per second) and throws an
ApplicationException that contains the full server path (potential path
disclosure). Replace the time()-based name with a securely unique temporary
directory name (e.g., using uniqid or a system temp utility) and create it
atomically (check for existence and retry or use a temp directory helper) and
change the exception message to a generic user-safe message (e.g., "Archive file
not found") that does not expose the server filesystem path; ensure directory
creation uses proper flags (recursive/permissions) as needed.

Comment on lines +160 to +231
public function onInstallUploadedPlugin(): string
{
try {
// Get the deferred binding record for the uploaded file
$widget = $this->getPackageUploadWidget();
$class = str_contains($class = Str::before(get_class($widget->model), chr(0)), '\\\\')
? str_replace('\\\\', '\\', $class)
: $class;

$deferred = DeferredBinding::query()
->where('master_type', 'LIKE', $class . '%')
->where('master_field', 'uploaded_package')
->where('session_key', $widget->getSessionKey())
->first();

// Attempt to get the file from the deferred binding
if (!$deferred || !$deferred->slave) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}

$file = $deferred->slave;
$localPath = $file->disk_name;
if (!FileHelper::copyBetweenDisks($file->getDisk(), 'temp', $file->getDiskPath(), $localPath)) {
throw new ApplicationException(Lang::get('system::lang.server.shit_gone_fucky'));
}

/**
* @TODO:
* - Process the uploaded file to identify the plugins to install
* - (optional) require confirmation to install each detected plugin
* - Install the identified plugins
* - Ensure that deferred binding records and uploaded files are removed post processing or on failure
*/

$manager = UpdateManager::instance();

// @TODO: Implement
// $result = $manager->installUploadedPlugin(Storage::disk('temp')->path($localPath));

if (!isset($result['code']) || !isset($result['hash'])) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}

$name = $result['code'];
$hash = $result['hash'];
$plugins = [$name => $hash];
$plugins = $this->appendRequiredPlugins($plugins, $result);

/*
* Update steps
*/
$updateSteps = $this->buildUpdateSteps(null, $plugins, [], true);

/*
* Finish up
*/
$updateSteps[] = [
'code' => 'completeInstall',
'label' => Lang::get('system::lang.install.install_completing'),
];

$this->vars['updateSteps'] = $updateSteps;

return $this->makePartial('execute');
}
catch (Exception $ex) {
// @TODO: Remove this, temporary debugging
throw $ex;
$this->handleError($ex);
return $this->makePartial('plugin_uploader');
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Uploaded-plugin install handler is incomplete and always fails; remove debug throw and fix placeholders.

  • $result is never set; the check will always fail.
  • throw $ex; in catch makes the later error handling unreachable.
  • Message key system::lang.server.shit_gone_fucky is inappropriate and likely missing.
  • $manager is unused.

Apply this interim hardening diff to avoid crashing until implementation lands:

@@
-            if (!FileHelper::copyBetweenDisks($file->getDisk(), 'temp', $file->getDiskPath(), $localPath)) {
-                throw new ApplicationException(Lang::get('system::lang.server.shit_gone_fucky'));
-            }
+            if (!FileHelper::copyBetweenDisks($file->getDisk(), 'temp', $file->getDiskPath(), $localPath)) {
+                throw new ApplicationException(Lang::get('system::lang.server.file_copy_failed'));
+            }
@@
-            $manager = UpdateManager::instance();
+            // $manager = UpdateManager::instance(); // TODO: use when implementing installUploadedPlugin()
@@
-            if (!isset($result['code']) || !isset($result['hash'])) {
-                throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
-            }
+            // TODO: Implement installUploadedPlugin() and populate $result; for now bail out gracefully.
+            throw new ApplicationException(Lang::get('system::lang.updates.not_implemented'));
@@
-        catch (Exception $ex) {
-            // @TODO: Remove this, temporary debugging
-            throw $ex;
-            $this->handleError($ex);
-            return $this->makePartial('plugin_uploader');
-        }
+        catch (Exception $ex) {
+            $this->handleError($ex);
+            return $this->makePartial('plugin_uploader');
+        }

Also remove the unused $name variable in details() as flagged by PHPMD.

🧰 Tools
🪛 PHPMD (2.15.0)

194-194: Avoid unused local variables such as '$manager'. (undefined)

(UnusedLocalVariable)

Comment on lines +238 to +297
public function onInstallPackage(): array
{
if (!$code = trim(post('package'))) {
throw new ApplicationException(Lang::get('system::lang.install.missing_package_name'));
}

if (!($type = trim(post('type'))) || !in_array($type, ['plugin', 'theme', 'module'])) {
throw new ApplicationException(Lang::get('system::lang.install.invalid_package_type'));
}

$key = base64_encode($this->cachePrefix . Session::getId() . md5(time() . $code));

App::terminating(function () use ($code, $type, $key) {
$output = new class extends BufferedOutput {
protected string $key;

protected function doWrite(string $message, bool $newline): void
{
Cache::put($this->key, Cache::get($this->key, '') . trim($message) . ($newline ? "\n" : ''));
}

public function setKey(string $key): void
{
$this->key = $key;
}
};

$output->setKey($key);

switch ($type) {
case 'plugin':
PluginManager::instance()->setOutput(new OutputStyle(new ArrayInput([]), $output));
break;
case 'theme':
ThemeManager::instance()->setOutput(new OutputStyle(new ArrayInput([]), $output));
break;
}

try {
$response = (new ComposerSource(match ($type) {
'plugin' => ExtensionSource::TYPE_PLUGIN,
'theme' => ExtensionSource::TYPE_THEME,
'module' => ExtensionSource::TYPE_MODULE,
}, composerPackage: $code))->install();
} catch (\Throwable $e) {
$response = null;
Cache::put($key, Cache::get($key, '') . 'ERROR: ' . implode(PHP_EOL, [
$e->getMessage(),
$e->getFile() . '@' . $e->getLine(),
$e->getTraceAsString(),
]));
} finally {
Cache::put($key, Cache::get($key, '') . 'FINISHED:' . ($response ? 'SUCCESS' : 'FAILED'));
}
});

return [
'install_key' => $key
];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid leaking stack traces to the UI; add TTL for cached logs and support module output.

  • Writing $e->getFile(), $e->getTraceAsString() to a cache that’s returned to the client exposes sensitive details.
  • No TTL set on Cache::put, causing unbounded growth if not cleaned.
  • Missing output routing for module installs.

Apply this diff:

@@
-            switch ($type) {
+            switch ($type) {
                 case 'plugin':
                     PluginManager::instance()->setOutput(new OutputStyle(new ArrayInput([]), $output));
                     break;
                 case 'theme':
                     ThemeManager::instance()->setOutput(new OutputStyle(new ArrayInput([]), $output));
                     break;
+                case 'module':
+                    \System\Classes\Extensions\ModuleManager::instance()->setOutput(new OutputStyle(new ArrayInput([]), $output));
+                    break;
             }
@@
-            } catch (\Throwable $e) {
-                $response = null;
-                Cache::put($key, Cache::get($key, '') . 'ERROR: ' . implode(PHP_EOL, [
-                    $e->getMessage(),
-                    $e->getFile() . '@' . $e->getLine(),
-                    $e->getTraceAsString(),
-                ]));
-            } finally {
-                Cache::put($key, Cache::get($key, '') . 'FINISHED:' . ($response ? 'SUCCESS' : 'FAILED'));
+            } catch (\Throwable $e) {
+                $response = null;
+                // Log full details server-side, return minimal error to client
+                \Log::error('[install] ' . $e->getMessage(), ['exception' => $e]);
+                Cache::put($key, Cache::get($key, '') . 'ERROR: ' . $e->getMessage() . PHP_EOL, now()->addMinutes(15));
+            } finally {
+                Cache::put($key, Cache::get($key, '') . 'FINISHED:' . ($response ? 'SUCCESS' : 'FAILED'), now()->addMinutes(15));
             }

Also set a TTL in doWrite() calls to avoid indefinite growth (see next comment).

🤖 Prompt for AI Agents
In modules/system/controllers/updates/traits/ManagesPlugins.php around lines 238
to 297, replace direct dumping of exception file/trace into the cache with a
safe, generic error string for the client and send the full exception to the
server log; ensure every Cache::put call (including inside the anonymous
BufferedOutput::doWrite and in the catch/finally blocks) supplies a TTL (e.g.
3600 seconds) to avoid indefinite growth; add a 'module' branch in the switch to
route output for module installs (similar to plugin/theme) so module install
output is captured; and in the catch block write only a non-sensitive message
like "ERROR: {message}" to the cache while logging $e via Log::error($e) or
similar.

Comment on lines +300 to +316
{
if (!$key = trim(post('install_key'))) {
throw new ApplicationException(Lang::get('system::lang.install.missing_plugin_name'));
}

if (!str_starts_with(base64_decode($key), $this->cachePrefix . Session::getId())) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}

$data = Cache::get($key, '');

return [
'done' => !$data || str_contains($data, 'FINISHED:SUCCESS') || str_contains($data, 'FINISHED:FAILED'),
'success' => str_contains($data, 'FINISHED:SUCCESS'),
'data' => $data
];
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Status endpoint marks empty output as “done”.

Treating empty cache as done causes immediate completion on the first poll.

Apply this diff:

-            'done' => !$data || str_contains($data, 'FINISHED:SUCCESS') || str_contains($data, 'FINISHED:FAILED'),
+            'done' => str_contains($data, 'FINISHED:SUCCESS') || str_contains($data, 'FINISHED:FAILED'),
             'success' => str_contains($data, 'FINISHED:SUCCESS'),

Also adjust the error message for a missing key:

-        if (!$key = trim(post('install_key'))) {
-            throw new ApplicationException(Lang::get('system::lang.install.missing_plugin_name'));
-        }
+        if (!$key = trim(post('install_key'))) {
+            throw new ApplicationException(Lang::get('system::lang.install.missing_install_key'));
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
if (!$key = trim(post('install_key'))) {
throw new ApplicationException(Lang::get('system::lang.install.missing_plugin_name'));
}
if (!str_starts_with(base64_decode($key), $this->cachePrefix . Session::getId())) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
$data = Cache::get($key, '');
return [
'done' => !$data || str_contains($data, 'FINISHED:SUCCESS') || str_contains($data, 'FINISHED:FAILED'),
'success' => str_contains($data, 'FINISHED:SUCCESS'),
'data' => $data
];
}
{
if (!$key = trim(post('install_key'))) {
throw new ApplicationException(Lang::get('system::lang.install.missing_install_key'));
}
if (!str_starts_with(base64_decode($key), $this->cachePrefix . Session::getId())) {
throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));
}
$data = Cache::get($key, '');
return [
'done' => str_contains($data, 'FINISHED:SUCCESS') || str_contains($data, 'FINISHED:FAILED'),
'success' => str_contains($data, 'FINISHED:SUCCESS'),
'data' => $data
];
}
🤖 Prompt for AI Agents
In modules/system/controllers/updates/traits/ManagesPlugins.php around lines
300–316, the endpoint currently treats an empty cache value as "done" and uses
an incorrect/misleading error message key for a missing install key. Change the
missing-key check to throw
ApplicationException(Lang::get('system::lang.install.missing_install_key')) (or
the correct language key for a missing install key), and update the return so
"done" is true only when the cached data contains FINISHED:SUCCESS or
FINISHED:FAILED (do not treat empty/blank cache as done); keep "success" true
only when FINISHED:SUCCESS and return the raw data as-is.

use Illuminate\Http\RedirectResponse;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Redirect;
use October\Rain\Exception\ApplicationException;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use Winter namespace for ApplicationException.

Line 10 imports October\Rain\Exception\ApplicationException, but Winter CMS should use Winter\Storm\Exception\ApplicationException for consistency with the framework's namespace.

-use October\Rain\Exception\ApplicationException;
+use Winter\Storm\Exception\ApplicationException;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use October\Rain\Exception\ApplicationException;
use Winter\Storm\Exception\ApplicationException;
🤖 Prompt for AI Agents
In modules/system/controllers/updates/traits/ManagesThemes.php around line 10,
the code imports October\Rain\Exception\ApplicationException but Winter CMS uses
the Winter namespace; replace that import with
Winter\Storm\Exception\ApplicationException so the correct ApplicationException
class from Winter\Storm is referenced and update any references if namespaced
aliasing is used.

Comment on lines +26 to +36
is_unfrozen:
label: system::lang.plugins.unfrozen
type: partial
path: is_unfrozen
sortable: false

is_enabled:
label: system::lang.plugins.enabled
type: partial
path: is_enabled
sortable: false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify partial files and language keys for new status columns.

The new is_unfrozen and is_enabled columns reference partial views and language keys that may not exist yet.


🏁 Script executed:

#!/bin/bash
# Verify partials and language keys for is_unfrozen and is_enabled columns

# Check for partial view files
echo "=== Checking for partial files ==="
fd -t f "_is_unfrozen.htm" modules/system/models/pluginversion/
fd -t f "_is_enabled.htm" modules/system/models/pluginversion/

# Check for language key definitions
echo "=== Checking for language keys ==="
rg -n "['\"](unfrozen|enabled)['\"]" modules/system/lang/ --type=php

# Check if these keys are defined in the plugins language file
rg -n "plugins\.unfrozen|plugins\.enabled" modules/system/lang/ --type=php

Length of output: 3132



---

🏁 Script executed:

```shell
#!/bin/bash
# Get broader context on partial files and directory structure

echo "=== Files in pluginversion directory ==="
fd -t f . modules/system/models/pluginversion/ | head -20

echo "=== All .htm files in pluginversion directory ==="
fd -t f "\.htm" modules/system/models/pluginversion/

echo "=== Search for is_unfrozen or is_enabled anywhere in codebase ==="
fd -t f -e htm | xargs grep -l "is_unfrozen\|is_enabled" 2>/dev/null || echo "No matches found"

echo "=== Check where other partials like is_active, status, etc. exist ==="
fd -t f "_is_" modules/system/models/pluginversion/

Length of output: 292



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where model partials are typically stored in the system module

echo "=== Search for _*.htm partial files in system module ==="
fd -t f "_.*\.htm" modules/system/ | head -15

echo "=== Check directory structure of system module ==="
fd -t d . modules/system/ -max-depth 3 | head -20

echo "=== Look for any partials or views directory in system ==="
fd -t d "partials|views|templates" modules/system/

Length of output: 706



---

🏁 Script executed:

```shell
#!/bin/bash
# Check contents of system/partials directory

echo "=== Files in modules/system/partials/ ==="
fd -t f . modules/system/partials/

echo "=== Look for any is_* partial files ==="
fd -t f "is_" modules/system/partials/

echo "=== Count how many partials exist in system/partials ==="
fd -t f . modules/system/partials/ | wc -l

Length of output: 327


Create missing partial view files for is_unfrozen and is_enabled columns.

The language keys are properly defined across all translations, but the referenced partial view files are missing. Create _is_unfrozen.htm and _is_enabled.htm in modules/system/partials/ to render these status columns.

🤖 Prompt for AI Agents
In modules/system/models/pluginversion/columns.yaml around lines 26 to 36 the
columns reference missing partials is_unfrozen and is_enabled; create the
partial view files modules/system/partials/_is_unfrozen.htm and
modules/system/partials/_is_enabled.htm that render the plugin status (use the
provided language keys system::lang.plugins.unfrozen and
system::lang.plugins.enabled), output appropriate status markup (e.g.,
icon/label classes for enabled/disabled and frozen/unfrozen states) and ensure
they accept the column value/context used by the List/Columns renderer so the
status displays correctly in the backend list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants