v2.5.0
Added
-
Theme base class for per-request behavior (#198) — Themes may now ship an optional
themes/{slug}/Theme.phpthat extendsArtisanPackUI\CMSFramework\Modules\Themes\Contracts\Themeto hook into the per-request lifecycle: enqueue front-end/editor CSS + JS, register custom image sizes, and register custom REST endpoints or block filters. Themes without aTheme.phpstay fully backward compatible. A new/themes/{slug}/assets/{path}route serves static assets from the theme'sassets/directory with slug/extension/traversal validation, an explicit MIME map (so CSS/JS aren't served astext/plain),nosniff, and CSP sandbox +Content-Dispositionfor SVG to close the stored-XSS vector on uploaded themes. The manifest-driventhemeClassoverride runs through aReflectionClassprovenance check so an uploadedtheme.jsoncannot instantiate an unrelated first-party or vendorThemesubclass. Three new filters —ap.themes.frontendStyles,ap.themes.editorStyles,ap.themes.frontendScripts— let third parties add or mutate enqueue lists without subclassing. -
Widened
GlobalStylesEmittercoverage against WPtheme.jsonv3 (#200, #201, #202) — Three closely-related emission gaps closed inGlobalStylesEmitter. The styles walker moves from a 5-key hardcoded map to a registry-based one coveringborder(radius/color/style/width, including per-corner and per-side shapes),spacing.padding/margin(scalar shorthand + per-side objects), extended typography (fontWeight/fontStyle/letterSpacing/textTransform/textDecoration), andshadow→box-shadow. The widened walker now feeds root, element, AND block rules, so per-element styles get the same coverage. A newblockStyleBlocks()emitsstyles.blocks.{ns/name}—core/quote→.wp-block-quote(namespace stripped to match Gutenberg),artisanpack/card→.wp-block-artisanpack-card. WP-canonicalvar:preset|category|slugshorthand is now translated into realvar(--wp--preset--category--slug)refs before emission (idempotent — rawvar(...)passes through unchanged).SCHEMA_VERSIONbumped v2 → v3 so cached CSS from prior deploys busts automatically. Follow-up hardening in the same wave:blockSelector()rejects block names with more than one slash (ns/foo/barused to explode into invalid.wp-block-ns-foo/barand silently drop the rule), andtranslatePresetValue()anchors its regex with(?![A-Za-z0-9_|-])sovar:preset|color|primary|garbagepasses through unchanged instead of half-translating. -
Populate blocks for theme patterns in
PatternResolver(#204) —PatternResolver::buildThemePattern()previously hardcodedblocks: [], which caused every theme-shipped pattern to render as an "Empty pattern" placeholder in the visual-editor pattern browser and open as an empty canvas until the client'shydrateBlocks()re-parsedcontent.raw. A newBlockMarkupParsersupport class ports a subset of WordPress'sparse_blocks()and wires into the theme-file branch. The parser produces the WPparse_blocks()shape ({blockName, attrs, innerBlocks, innerHTML, innerContent}) and covers flat blocks, nested containers, void (self-closing) blocks, freeform HTML between blocks, and forgiving JSON-attr decoding. Client-side Gutenbergparse(rawContent)remains authoritative for editor correctness — this parser is a best-effort optimization for lightweight surfaces like the pattern thumbnail summary. Recursion is capped at depth 64 so adversarially-nestedblock_contentcan't blow the PHP call stack; PCRE errors and JSON-decode failures are logged viaLog::warning; a leading UTF-8 BOM is stripped so BOM-prefixed theme files don't emit a phantom freeform sibling. The parser is deliberately generic — siblingTemplateResolverandTemplatePartResolvertheme-file branches ship the same gap and can drop this in as-is. -
Per-block-element overrides under
styles.blocks.{name}.elements.*(#208) — ExtendsGlobalStylesEmitterto recurse into each block'selementsmap after emitting the base block rule, composing the block selector with each supported element selector so per-block-element overrides render identically to Gutenberg's own emission. Comma-joined element selectors (h1, h2, …) are distributed across the block selector —.wp-block-quote h1, .wp-block-quote h2, …— never naively concatenated as.wp-block-quote h1, h2, …, which would leak the child selector out of block scope.SCHEMA_VERSIONbumped v3 → v4 so the cache invalidates on deploy. -
Post / Page lifecycle, dashboard-widgets, plugin.hookRegistered, and search-query hooks (Wave 5) (#196) — Thirteen new fire sites bring the CMS Framework's hook surface up to parity with the rest of the ecosystem. Each
PostandPagemodel now emitsap.cmsFramework.{post,page}.saving,.saved,.published(fires only on a transition intoContentStatus::Published— first-save-as-published, draft→published, and scheduled→published all count; subsequent saves of an already-published record do not),.trashed(soft delete only — force-delete is skipped), and.restoredvia the newFiresLifecycleHooksconcern undersrc/Modules/ContentTypes/Models/Concerns/.AdminWidgetManager::getAvailableWidgetsForUser()now runs its output throughap.cmsFramework.admin.dashboardWidgets, passing the resolved user (ornull) so subscribers can make per-user injections without re-resolving auth.PluginManager::activate()firesap.cmsFramework.plugin.hookRegisteredimmediately after the plugin's service provider registers, carrying(string $pluginSlug, array $hooks)— the hooks array is the optionalhooksfield from the plugin'splugin.json(empty array when absent) so observers still get a per-plugin signal.HasContentFilters::applySearchFilter()(used by bothBlogManager::getArchiveQuery()andPageManager::getPageQuery()) runs the assembled search query throughap.cmsFramework.search.querywith(Builder $q, string $term, array $context)where$contextcarries the calling manager class, the queried model class, and the full filter array — enough for a subscriber to swap in a full-text index or route to an external search service. -
Editor-only theme stylesheet (#199) — Themes may now ship an optional
themes/{slug}/editor.cssalongside the existingthemes/{slug}/style.css. TheGET /api/v1/global-styles/cssendpoint concatenates emitter output +style.css+editor.css(in that order), giving the site-editor canvas the full canvas stylesheet in a single fetch. The@cmsGlobalStylesBlade directive is unchanged — it renders only the emitter output — soeditor.cssnever leaks to the public front-end. Analog to WordPress'sadd_editor_style(); lets themes use bare element selectors for canvas-only overrides without theming inspector-panel mini-previews. -
ThemeStylesheetReadersupport class (#199) — Public, container-bound reader (app( ThemeStylesheetReader::class )) that safely resolvesthemes/{slug}/{filename}for the active theme. Slug validation delegates toThemeManager::validateSlug(), path resolution toThemeManager::getThemesPath(), and traversal containment to the newPathContainmentGuard. MemoizesgetActiveTheme()per instance so multiple reads inside a request pay the schema-validation cost once.frontendStylesheet()/editorStylesheet()are convenience wrappers around a genericread( string $filename );readWrapped( $filename )returns the contents behind a/* === filename === */banner for concatenation, andlastModified()exposes the freshest theme-stylesheet mtime for cache-key composition. Notfinal— downstream packages (packages/visual-editor) can extend or fake it. -
PathContainmentGuard::within( $base, $candidate )(#199) — Shared realpath +str_starts_withcontainment helper for the security-sensitive path-traversal guard that was previously inlined inThemeStylesheetReader,ThemeAssetsController, and the sibling visual-editor controller. Returns the canonicalized absolute path when the candidate lives inside$base, null otherwise. -
HTTP caching on
/api/v1/global-styles/css(#199) — Response now carries anETagderived from the concatenated body and honorsIf-None-Matchwith a 304, plusCache-Control: private, must-revalidateso intermediaries revalidate cheaply instead of serving stale bytes when a theme editsstyle.css/editor.css. -
ThemeManager::getThemesPath()andvalidateSlug()public (#199) — Both accessors are now public so downstream helpers (like the new stylesheet reader and any package building on it) can anchor to the same themes root and slug rule instead of duplicating the resolution logic.getThemesPath()also gains absolute-path handling: an absolute value incms.themes.directory(e.g./opt/themes) is now honored verbatim instead of being prepended withbase_path(), and anull/empty configured value falls back tothemesso the containment guard never collapses to the app root.
Changed
- Normalized hook namespaces to
ap.cmsFramework.*(#193, #194, #195) — Roughly 120 hook names emitted by the CMS Framework have been renamed onto a consistent namespace. Wave 4a covers infrastructure surfaces (ap.admin.menu→ap.cmsFramework.admin.menu, the threeap.*.enqueuedAssetsfamilies, theap.admin.contentEdit.*extension surfaces,ap.dynamic_content.register-types→ap.cmsFramework.dynamicContent.registerTypes, plusap.roleRegistered/ap.permissionRegisteredmoving toap.rbac.*). Wave 4b covers lifecycle events (plugin.installing/installed/activating/activated/deactivating/deactivated/deleting/deleted/updating/updated→ap.cmsFramework.plugin.<action>;theme.installing/installed/activating/activated→ap.cmsFramework.theme.<action>) and the comment surfaces (comment.editLink,comments.store.defaultStatus,comments.rate-limit.{guest,authenticated},comments.form.actionall move underap.cmsFramework.comment{s}.*). Wave 4c namespaces every policy-level ability filter ({resource}.{action}such asposts.view,pages.publish,comments.moderate,role.forceDelete) underap.cmsFramework.abilities.{resource}.{action}. - Requires
artisanpack-ui/hooks: ^1.3for the newdeprecateHook()alias primitive that backs this change.
Deprecated
- Every pre-2.5.0 hook name renamed above is registered as an alias via the new
ArtisanPackUI\CMSFramework\Support\HookAliases::register()primitive (invoked inCMSFrameworkServiceProvider::boot()). Existing subscribers on the old names keep firing, and callbacks registered on old vs. new names dispatch together — no host-app changes are required to upgrade, but the old names will emit a one-per-request deprecation log entry so downstreams can migrate at their own pace.