Repository navigation
v2.16.0
·
6 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
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 requirein the host root and writes the packages into the host'scomposer.jsonandcomposer.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:rollbackor the queue worker behinddispatchUpdate()could only clear the CLI's own OPcache. It logged a warning and setphp_reload_required, and on a host withopcache.validate_timestamps=0PHP-FPM kept serving a mix of old and new code until someone reloaded it. After a CLI or queued run,ApplicationUpdateManagernow 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: setcms.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, oncms.updates.opcache.loopback_url(defaultapp.url); the route is exempt from maintenance mode. Last it runscms.updates.php_reload_command(CMS_UPDATES_PHP_RELOAD_COMMAND), if one is configured. The mechanism that worked is recorded asopcache_reset_methodin the update state, shown byupdate:status, and returned by the newApplicationUpdateManager::opcacheResetMethod(). Only when all three fail does the updater fall back to the warning andphp_reload_required, and the log now says why each one failed.update:performandupdate:rollbackalso print that case more prominently. Each mechanism can be turned off by settingCMS_UPDATES_OPCACHE_FASTCGIorCMS_UPDATES_OPCACHE_LOOPBACKtofalseor0; 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
composerblock was only acted on at activation, and only installed whencms.plugins.autoInstallComposerDependencieswastrue, 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 rancomposer requireby hand, which an unattended install can't do. NowinstallFromZip()andinstallFromDisk()install the unmet packages once the manifest and slug checks pass, before the plugin's row is created, andUpdateManager::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 aPluginInstallationExceptionwith 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 acomposerblock now always runs the idempotent packagemigrate --force, because packages installed at install time could otherwise never have their migrations run. The resolve-and-install logic is shared by the new publicPluginManager::installComposerDependenciesForManifest().- Upgrade (see the warning above):
autoInstallComposerDependenciesnow defaults totrue. A host that leaves it unset gets plugin dependencies installed withcomposer requirein 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'scomposer.jsonand 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 setPLUGINS_AUTO_INSTALL_COMPOSER_DEPENDENCIES=false, which keeps exactly the old behavior: install and update don't touch thecomposerblock, and activation fails closed.
- Upgrade (see the warning above):
Fixed
- Site-editor reverts and menu unassigns now fire Eloquent
deletedevents and a newap.cmsFramework.siteEditor.revertedaction (#423).TemplateResolver::revert(),TemplatePartResolver::revert(),GlobalStylesResolver::revert()andMenus::unassign()(used byMenuResolver::revert()) deleted the row with a query-leveldelete(), which fires no model event. A listener oneloquent.saved/eloquent.deletedfor these models saw every save but no revert, so its caches went stale, and a workaround that watched theDELETEroutes missed reverts from the console, jobs or direct calls. Each method now loads the row and deletes it through the model, sodeletinganddeletedfire. After a successful delete it firesap.cmsFramework.siteEditor.revertedwith(string $type, ?string $slug, string $theme).$typeistemplate,template-part,global-stylesormenu-location, and$slugisnullfor global styles. Adeletinglistener that returnsfalsenow cancels the revert; the method then returnsfalseand 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'scomposerpackages (the flag was off at install, the package went missing later, or the plugin predates 2.16), it ran the packagemigrate --forcein the same request. That request had booted from the old package manifest, so the new package's provider never ranloadMigrationsFrom(), the migrate found nothing to do, and the package's tables were missing until someone ranphp artisan migrateby hand. The migrate now runs in a freshphp artisan migrate --forceprocess, 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, thenPHP_BINARYunder 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 fromApplicationUpdateManagerinto the newResolvesCliPhpBinarytrait, which both classes use; method names and behavior are unchanged.