v2.4.0
Added
-
Dynamic content system (#178) — Site-wide dynamic content module at
src/Modules/DynamicContent/. Site owners define a piece of content once and reference it anywhere via{{source.field|mod:arg}}merge tokens; edits propagate everywhere the token is used. Ships four migrations (dynamic_content_types,_fields,_records,_record_values); an in-memory registry that merges admin-created (DB) and code-registered types with slug-conflict warnings; 11 built-in field types (text, rich_text, url, email, phone, image, date, datetime, number, address, select) with HTML-safe rendering (text-shaped types escape at render time, rich text stays raw by design); 7 built-in modifiers (default, upper, lower, truncate, date, time, nl2br —nl2brescapes input before inserting<br>and returnsHtmlString); aDynamicContentResolverwith a 65KB/resolvecap plus throttle; theapRenderContent()helper and@dynamicContentBlade directive; aDynamicContentCastEloquent cast (documented as a public content pipeline — do NOT store secrets in dynamic content records); filter-driven authz (manage_dynamic_contentcapability with per-type policy scoping viaviewAnyForType/createForType); per-request memoization on the accessor to eliminate collection-token N+1s; a resolver signature cache invalidated on record save/delete; REST endpoints under/api/v1/dynamic-content/*with types bound by slug; a Livewire admin surface (type list, field builder, singleton editor, collection editor); and wiring intoHasRenderedBlockContent::renderContent()so Blog Post, Pages, and any content type using the trait auto-resolve tokens. -
Plugin system v2.4 — schema, filter, base provider, compat, lifecycle (#179, #180, #181, #182, #183, #190) — Five interrelated enhancements for the Keystone CMS plugin UI initiative:
plugin.jsonschema extended withmin_host_version,federated_module,nav_entries,permissions(plugin-slug-namespaced),migrations_path(traversal-guarded), androllback_migrations_on_delete.- New
ap.admin.menufilter with a React/Inertia-friendly row shape (url/label/iconId/permission/external); post-filter capability re-check gates plugin-injected entries. - New
PluginServiceProviderbase class withregisterAdminPage/registerNavEntry/registerFederatedModule/pluginPath/pluginConfighelpers, backed by a container-boundPluginRegistrysingleton (Octane-safe). - Plugin
min_host_versionenforced at activation viaIncompatiblePluginExceptionwithcomposer.json+InstalledVersionsresolution; controller surfaces a structured 409 response. - Complete plugin lifecycle: transactional activation with PSR-4 snapshot/restore rollback, opt-in migration rollback on delete, namespace-scoped permission seed/unseed, and optional framework cache clears (
cms.plugins.autoClearFrameworkCaches, defaultfalse).
-
Complete plugin extensibility (#184, #185, #186, #187, #188, #191) — Ships the remaining plugin-system issues so third-party plugins can extend the framework end-to-end:
ContentTypeManager/TaxonomyManagersingular getters (getContentType,contentTypeExists,getTaxonomy,taxonomyExists,getTaxonomiesForContentType) now hydrate filter-registered entries as unpersisted models (backwards compatible via Option B); addedgetPersistedContentType/getPersistedTaxonomyfor privileged callers that must never trust plugin-suppliedtable_name.- New
CustomFieldTypeRegistry+FieldTypeDefinitionvalue object undersrc/Modules/ContentTypes/. All 16 built-inFieldTypeenum cases are pre-registered. NewapRegisterFieldType(string $slug, array|FieldTypeDefinition $definition)helper matches the siblingapRegister*signature convention.CustomField.typecast changed fromFieldType::classtostring; newfieldTypeEnum(),fieldTypeDefinition(),storageMode()accessors.CustomFieldRequestnow validates againstRule::in(registry->slugs()). CustomFieldManager::getFieldsForContentType()merges DB + filter fields. Filter fields carrystorage = metadata. TheHasCustomFieldstrait was rewritten to route metadata-storage fields to the model'smetadataJSON column. Schema mutations (createField,updateField,deleteField,generateMigration) are now restricted to DB-persisted content types so a plugin's filter-registeredtable_namecan never steerSchema::tableat a host table.- New
ContentEditExtensionsmanager exposingpanels(),tabs(),beforeEditor(),afterEditor(),saveData()overap.admin.contentEdit.{panels,tabs,beforeEditor,afterEditor,saveData}. RespectscontentTypesrestriction andorder. - Plugin authoring guide (
docs/plugin-authoring.md) andexamples/hello-world-plugin/skeleton withplugin.json, basePluginServiceProviderusage, migration, and Blade admin view. Federated React flavor stubbed with pointer to Keystone.READMEplugin-system section rewritten; "experimental" wording removed.
Security
- Plugin system hardening (#190, #191) — surfaced by local
/code-reviewpasses:- Reject manifest
migrations_pathtraversal (schema + runtimerealpathcheck). - Namespace-prefix permission slugs so seed/unseed cannot touch core or other plugin rows.
- Re-apply
Gate::allowsafter theap.admin.menufilter runs. - Allow-list URL schemes in
registerNavEntryandresolveRouteUrl. - Constrain
PluginServiceProvidermanifest walk to direct children of the plugins root. HasCustomFieldsshort-circuits viagetCasts()+ realSchemacolumn listing so a plugin cannot shadow a host attribute (password,is_admin,metadata) by registering a filter-scoped field with a colliding key.__seton filter-registered fields throwsRuntimeExceptionwhen the host model has nometadatacolumn instead of silently dropping writes.update/deleteContentTypeandupdate/deleteTaxonomyrefuse filter-only slugs (previously silentlyINSERTed phantom rows via Eloquentupdate()onexists = false).forceFillpayloads stripid/created_at/updated_at/deleted_atso plugins cannot seed persistence-critical keys.CustomFieldTypeRegistryrejects malformed filter entries (non-scalarslugorlabel, emptyslug) — one bad plugin no longer DoSes the registry.getCustomFieldsForTypeis memoized per instance and auto-flushed onsaved/deleted.
- Reject manifest