Skip to content

v2.5.0

Choose a tag to compare

@github-actions github-actions released this 21 Jul 21:22
· 159 commits to main since this release
Immutable release. Only release title and notes can be modified.
a4609cc

Added

  • Theme base class for per-request behavior (#198) — Themes may now ship an optional themes/{slug}/Theme.php that extends ArtisanPackUI\CMSFramework\Modules\Themes\Contracts\Theme to 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 a Theme.php stay fully backward compatible. A new /themes/{slug}/assets/{path} route serves static assets from the theme's assets/ directory with slug/extension/traversal validation, an explicit MIME map (so CSS/JS aren't served as text/plain), nosniff, and CSP sandbox + Content-Disposition for SVG to close the stored-XSS vector on uploaded themes. The manifest-driven themeClass override runs through a ReflectionClass provenance check so an uploaded theme.json cannot instantiate an unrelated first-party or vendor Theme subclass. Three new filters — ap.themes.frontendStyles, ap.themes.editorStyles, ap.themes.frontendScripts — let third parties add or mutate enqueue lists without subclassing.

  • Widened GlobalStylesEmitter coverage against WP theme.json v3 (#200, #201, #202) — Three closely-related emission gaps closed in GlobalStylesEmitter. The styles walker moves from a 5-key hardcoded map to a registry-based one covering border (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), and shadowbox-shadow. The widened walker now feeds root, element, AND block rules, so per-element styles get the same coverage. A new blockStyleBlocks() emits styles.blocks.{ns/name}core/quote.wp-block-quote (namespace stripped to match Gutenberg), artisanpack/card.wp-block-artisanpack-card. WP-canonical var:preset|category|slug shorthand is now translated into real var(--wp--preset--category--slug) refs before emission (idempotent — raw var(...) passes through unchanged). SCHEMA_VERSION bumped 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/bar used to explode into invalid .wp-block-ns-foo/bar and silently drop the rule), and translatePresetValue() anchors its regex with (?![A-Za-z0-9_|-]) so var:preset|color|primary|garbage passes through unchanged instead of half-translating.

  • Populate blocks for theme patterns in PatternResolver (#204) — PatternResolver::buildThemePattern() previously hardcoded blocks: [], 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's hydrateBlocks() re-parsed content.raw. A new BlockMarkupParser support class ports a subset of WordPress's parse_blocks() and wires into the theme-file branch. The parser produces the WP parse_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 Gutenberg parse(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-nested block_content can't blow the PHP call stack; PCRE errors and JSON-decode failures are logged via Log::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 — sibling TemplateResolver and TemplatePartResolver theme-file branches ship the same gap and can drop this in as-is.

  • Per-block-element overrides under styles.blocks.{name}.elements.* (#208) — Extends GlobalStylesEmitter to recurse into each block's elements map 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_VERSION bumped 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 Post and Page model now emits ap.cmsFramework.{post,page}.saving, .saved, .published (fires only on a transition into ContentStatus::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 .restored via the new FiresLifecycleHooks concern under src/Modules/ContentTypes/Models/Concerns/. AdminWidgetManager::getAvailableWidgetsForUser() now runs its output through ap.cmsFramework.admin.dashboardWidgets, passing the resolved user (or null) so subscribers can make per-user injections without re-resolving auth. PluginManager::activate() fires ap.cmsFramework.plugin.hookRegistered immediately after the plugin's service provider registers, carrying (string $pluginSlug, array $hooks) — the hooks array is the optional hooks field from the plugin's plugin.json (empty array when absent) so observers still get a per-plugin signal. HasContentFilters::applySearchFilter() (used by both BlogManager::getArchiveQuery() and PageManager::getPageQuery()) runs the assembled search query through ap.cmsFramework.search.query with (Builder $q, string $term, array $context) where $context carries 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.css alongside the existing themes/{slug}/style.css. The GET /api/v1/global-styles/css endpoint concatenates emitter output + style.css + editor.css (in that order), giving the site-editor canvas the full canvas stylesheet in a single fetch. The @cmsGlobalStyles Blade directive is unchanged — it renders only the emitter output — so editor.css never leaks to the public front-end. Analog to WordPress's add_editor_style(); lets themes use bare element selectors for canvas-only overrides without theming inspector-panel mini-previews.

  • ThemeStylesheetReader support class (#199) — Public, container-bound reader (app( ThemeStylesheetReader::class )) that safely resolves themes/{slug}/{filename} for the active theme. Slug validation delegates to ThemeManager::validateSlug(), path resolution to ThemeManager::getThemesPath(), and traversal containment to the new PathContainmentGuard. Memoizes getActiveTheme() per instance so multiple reads inside a request pay the schema-validation cost once. frontendStylesheet() / editorStylesheet() are convenience wrappers around a generic read( string $filename ); readWrapped( $filename ) returns the contents behind a /* === filename === */ banner for concatenation, and lastModified() exposes the freshest theme-stylesheet mtime for cache-key composition. Not final — downstream packages (packages/visual-editor) can extend or fake it.

  • PathContainmentGuard::within( $base, $candidate ) (#199) — Shared realpath + str_starts_with containment helper for the security-sensitive path-traversal guard that was previously inlined in ThemeStylesheetReader, 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 an ETag derived from the concatenated body and honors If-None-Match with a 304, plus Cache-Control: private, must-revalidate so intermediaries revalidate cheaply instead of serving stale bytes when a theme edits style.css / editor.css.

  • ThemeManager::getThemesPath() and validateSlug() 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 in cms.themes.directory (e.g. /opt/themes) is now honored verbatim instead of being prepended with base_path(), and a null/empty configured value falls back to themes so 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.menuap.cmsFramework.admin.menu, the three ap.*.enqueuedAssets families, the ap.admin.contentEdit.* extension surfaces, ap.dynamic_content.register-typesap.cmsFramework.dynamicContent.registerTypes, plus ap.roleRegistered / ap.permissionRegistered moving to ap.rbac.*). Wave 4b covers lifecycle events (plugin.installing/installed/activating/activated/deactivating/deactivated/deleting/deleted/updating/updatedap.cmsFramework.plugin.<action>; theme.installing/installed/activating/activatedap.cmsFramework.theme.<action>) and the comment surfaces (comment.editLink, comments.store.defaultStatus, comments.rate-limit.{guest,authenticated}, comments.form.action all move under ap.cmsFramework.comment{s}.*). Wave 4c namespaces every policy-level ability filter ({resource}.{action} such as posts.view, pages.publish, comments.moderate, role.forceDelete) under ap.cmsFramework.abilities.{resource}.{action}.
  • Requires artisanpack-ui/hooks: ^1.3 for the new deprecateHook() 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 in CMSFrameworkServiceProvider::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.

Removed

Fixed

Security