Skip to content

v2.16.0

Choose a tag to compare

@github-actions github-actions released this 25 Oct 22:38
· 6 commits to main since this release
Immutable release. Only release title and notes can be modified.
0be46a3

Warning

Security-relevant default change: plugins now install their Composer dependencies by default, at install time. cms.plugins.autoInstallComposerDependencies now defaults to true, and the packages a plugin declares in its composer block are installed when the plugin is installed or updated, not only when it is activated (#430). This means:

  • Uploading a plugin ZIP, even one you never activate, runs composer require in the host root and writes the packages into the host's composer.json and composer.lock.
  • Laravel's package discovery then registers those packages' service providers, which run on every request, site-wide, even while the plugin itself is inactive.
  • Anyone who can install or update plugins can therefore bring new third-party code into the host.

To keep the pre-2.16 behavior (no runtime Composer installs; activation fails closed on an unmet requirement), set PLUGINS_AUTO_INSTALL_COMPOSER_DEPENDENCIES=false. Do this on any host where plugin installers aren't fully trusted, or where Composer can't run at runtime (read-only filesystems, immutable container images).

Changed

  • CLI and queued self-updates now reset PHP-FPM's OPcache instead of only warning (#429). Since 2.15.6, an update or rollback run from update:perform, update:rollback or the queue worker behind dispatchUpdate() could only clear the CLI's own OPcache. It logged a warning and set php_reload_required, and on a host with opcache.validate_timestamps=0 PHP-FPM kept serving a mix of old and new code until someone reloaded it. After a CLI or queued run, ApplicationUpdateManager now tries three ways to reach FPM and stops at the first that works. First it runs a one-off reset script through the FPM socket over FastCGI: set cms.updates.opcache.fpm_socket (CMS_UPDATES_FPM_SOCKET), or the usual Forge, Debian/Ubuntu and RHEL socket paths are tried. Next it requests a one-minute signed loopback route, /cms-framework/updates/opcache-reset, on cms.updates.opcache.loopback_url (default app.url); the route is exempt from maintenance mode. Last it runs cms.updates.php_reload_command (CMS_UPDATES_PHP_RELOAD_COMMAND), if one is configured. The mechanism that worked is recorded as opcache_reset_method in the update state, shown by update:status, and returned by the new ApplicationUpdateManager::opcacheResetMethod(). Only when all three fail does the updater fall back to the warning and php_reload_required, and the log now says why each one failed. update:perform and update:rollback also print that case more prominently. Each mechanism can be turned off by setting CMS_UPDATES_OPCACHE_FASTCGI or CMS_UPDATES_OPCACHE_LOOPBACK to false or 0; turning off the loopback also unregisters its route.
  • A plugin's Composer dependencies are installed when the plugin is installed, and that is now on by default (#430). A plugin's composer block was only acted on at activation, and only installed when cms.plugins.autoInstallComposerDependencies was true, which it wasn't by default. So out of the box a plugin with a Composer dependency installed fine and then couldn't be activated until someone ran composer require by hand, which an unattended install can't do. Now installFromZip() and installFromDisk() install the unmet packages once the manifest and slug checks pass, before the plugin's row is created, and UpdateManager::updatePlugin() does the same after extracting a new version, whether or not the plugin is active. If the packages can't be installed, the install throws a PluginInstallationException with Composer's reason and no row is created (a ZIP install also removes its extracted files; installFromDisk() leaves the directory alone), and an update rolls back through the backup-restore path. Freshly installed packages are seated on the running class loader, so a plugin activated in the same request can load them. Activation still resolves the block as a backstop, with the same guards as before: requirements are re-validated, a package the host itself requires is refused, and an install this process can't verify fails closed. With auto-install on, activating a plugin that declares a composer block now always runs the idempotent package migrate --force, because packages installed at install time could otherwise never have their migrations run. The resolve-and-install logic is shared by the new public PluginManager::installComposerDependenciesForManifest().
    • Upgrade (see the warning above): autoInstallComposerDependencies now defaults to true. A host that leaves it unset gets plugin dependencies installed with composer require in the host root at install, update and activation, instead of activation failing closed. Because this happens at install time, an installed but inactive plugin's packages are in the host's composer.json and their auto-discovered service providers boot on every request. Hosts that don't fully trust everyone who can install plugins, or that can't run Composer at runtime (read-only filesystems, immutable container images), should set PLUGINS_AUTO_INSTALL_COMPOSER_DEPENDENCIES=false, which keeps exactly the old behavior: install and update don't touch the composer block, and activation fails closed.

Fixed

  • Site-editor reverts and menu unassigns now fire Eloquent deleted events and a new ap.cmsFramework.siteEditor.reverted action (#423). TemplateResolver::revert(), TemplatePartResolver::revert(), GlobalStylesResolver::revert() and Menus::unassign() (used by MenuResolver::revert()) deleted the row with a query-level delete(), which fires no model event. A listener on eloquent.saved / eloquent.deleted for these models saw every save but no revert, so its caches went stale, and a workaround that watched the DELETE routes missed reverts from the console, jobs or direct calls. Each method now loads the row and deletes it through the model, so deleting and deleted fire. After a successful delete it fires ap.cmsFramework.siteEditor.reverted with (string $type, ?string $slug, string $theme). $type is template, template-part, global-styles or menu-location, and $slug is null for global styles. A deleting listener that returns false now cancels the revert; the method then returns false and the action doesn't fire.
  • Migrations of a plugin's Composer packages now run when activation is what installs them (#431). When activate() installed a plugin's composer packages (the flag was off at install, the package went missing later, or the plugin predates 2.16), it ran the package migrate --force in the same request. That request had booted from the old package manifest, so the new package's provider never ran loadMigrationsFrom(), the migrate found nothing to do, and the package's tables were missing until someone ran php artisan migrate by hand. The migrate now runs in a fresh php artisan migrate --force process, which boots the regenerated manifest and the now-active plugin's provider. It uses the same CLI PHP resolution as the self-updater (CMS_PHP_BINARY, then PHP_BINARY under the CLI, then the Herd, Homebrew and system paths), so it works when activation runs under PHP-FPM. If that process fails, activation rolls back and the exception carries the migrate output. The same applies when the packages were installed earlier in the same process, as when an update of an active plugin installs the new version's packages and then reactivates it straight away; that first activation also migrates in a fresh process. Activations whose packages were installed in an earlier request still migrate in-process, as before. The PHP resolution moved from ApplicationUpdateManager into the new ResolvesCliPhpBinary trait, which both classes use; method names and behavior are unchanged.