Tags give the ability to mark specific points in history as being important
-
1.3.0-beta5
d9120aaf · ·1.3.0-beta5 Second cache-correctness fix from the 1.3.0-beta4 retest on portal-master. The amended canary protocol (admin + editor + anon) confirmed beta4's BigPipe suppression resolved scenario (a) but surfaced a second leakage pattern: with private-cache mode on the homepage cache keyed identically for anonymous and authenticated requests, so whichever variant populated the cache first was served to everyone. Root cause was on the cache-key side: the module emitted X-LiteSpeed-Cache-Control: private but did not tell LSWS to vary the cache entry on the session cookie. The reporter shipped a patch and a 27-request canary confirming the fix. beta5 lands the patch plus the obvious follow-ups. - Response subscriber auto-appends the session cookie name to the X-LiteSpeed-Vary header whenever private_cache.enabled = TRUE. Cookie name discovered via SessionConfiguration so the fix needs no new config keys. - Three new unit tests in LscacheResponseSubscriberTest cover the auto-append, the no-append path in public-only mode, and the deduplication path. - New status report row parses CacheVary cookie= directives out of .htaccess and warns when they don't match the emitted cookies, with a copy-pasteable suggested directive. - New docs section in docs/htaccess-gotchas.md on authenticated cache differentiation, with a drush ev one-liner to discover the session cookie name and an example IfModule LiteSpeed block. Pure cache-correctness fix; no API contract changes. CI: pipeline 821549 green (phpunit, phpcs, cspell, phpstan, eslint, composer-lint, composer all passing).
-
1.3.0-beta4
be1d4d9b · ·1.3.0-beta4: suppress LSCache caching when BigPipe placeholders are present Cache-correctness fix from the 1.3.0-beta3 retest on portal-master. The retest reporter corrected an earlier miscall on Finding 7 (the auth-served-as-anonymous variant from the rc1 hand-back): the original canary protocol used authenticated-role-only test users with no visible per-user markup beyond what BigPipe streams, so the leak-detector saw identical responses regardless of whether the authenticated variant or the cached anonymous variant was served. The same operator hitting the canonical homepage as admin saw the anonymous-shape body; hitting a cache-busted URL produced the admin-shape body. Scenario (b) outcome: a real cache-correctness gap, not a downstream artifact of Finding 1. Root cause: BigPipe's Drupal core implementation resolves placeholders at streaming time inside PHP. LSWS serves cached responses without invoking PHP, so BigPipe never runs on cache hits and the placeholders stay unresolved. Authenticated content rendered via BigPipe (admin toolbar, user-account widgets, cached responses. What landed since 1.3.0-beta3 (commit 5593f00): - 86ed7f8: response subscriber scans the response body for `data-big-pipe-placeholder-id` markers. When detected, emits X-LiteSpeed-Cache-Control: no-cache to actively suppress LSWS-side caching. Active suppression is necessary because the HTTP Cache-Control is still public,max-age=N (the response is HTTP-cacheable in the abstract); a soft skip would let LSWS cache via its own header parsing anyway. - 39de4bc: three new unit tests on LscacheResponseSubscriberTest. testBigPipePlaceholderTriggersActiveNoCacheSuppression covers the suppression on the public branch. testBigPipePlaceholder SuppressesEvenInPrivateMode covers the private branch (BigPipe responses with user contexts that would otherwise qualify for private-cache emission still get suppressed because the private hit path also bypasses PHP). testNonBigPipeResponseEmitsPublic HeadersAsBefore is the regression guard verifying non-BigPipe responses continue to cache normally. - c5daa48: new docs section in htaccess-gotchas.md on the BigPipe interaction. Explains the streaming-vs-static incompatibility, documents the automatic suppression behaviour, and points operators with per-user content needs at the #type: 'lscache_esi' migration path with a before/after render-array example. - be1d4d9: ROADMAP records the beta4 cycle and the corrected Finding 7 attribution. Pure cache-correctness fix; no API contract changes. The HTTP 200 = SUCCEEDED mapping in the purger is unchanged. Operators who hit the beta3 BigPipe symptom can re-enable LSCache once on beta4 and re-run the amended canary protocol (admin role + editor role + anon, explicit checks for toolbar markup and BigPipe placeholder resolution). Deferred to 1.3.1 / 1.4.x (unchanged from beta3): post-purge sentinel verification flag, `everything` invalidation type, and the operator-experience polish items still in the queue. Soak target: clean re-test on the amended canary protocol from the env session; if clean, promotes straight to rc, then ~2 weeks soak, then 1.3.0 stable.
-
1.3.0-beta3
5593f009 · ·1.3.0-beta3: operator-experience fixes from the rc2 production-soak Stepping back from rc to beta after the 1.3.0-rc2 install reached the actual PURGE code path for the first time on a Cloudflare- fronted Jelastic-managed LSWS Enterprise environment and surfaced operator-experience gaps that warranted a fresh soak window before re-claiming rc quality. What landed since 1.3.0-rc2 (commit f037d9b): - 442e93f: actionable error messages. Suggested URL in the no-host failure reason changed from https://your-site.example.com to http://localhost/ (the previous example sent operators behind any CDN into a 400 trap). HTTP non-2xx purge failures now surface operator-actionable reasons via the new formatNon2xxReason() helper, detecting Cloudflare's HTTP 400 + CF-Ray signature specifically and pointing operators at origin-direct purge_host alternatives. Same change applied to the settings form description and placeholder. - 88d4398: purger logging now routes through @logger.channel. lscache_purger via constructor injection. The channel was registered in services.yml since 1.0.0 but never used; the plugin defaulted to PurgerBase's framework channel. drush watchdog:show --type=lscache_purger now returns purge-warning entries instead of "Unrecognized message type". Messenger-surfaced failure reasons now apply to every failure path (no-host, HTTP non-2xx, transport error), not just the no-host case rc2 covered. - 89a1150: two new doc sections in docs/htaccess-gotchas.md. "LSWS-side PURGE handling" makes explicit that HTTP 200 from LSWS means "request accepted at the protocol level," not "entry evicted." On managed-LSWS environments (Jelastic, Cloudways) and on Enterprise installs with admin-port routing, the two can be distinct states. Documents the diagnostic, LSWS configuration requirements, and operator workarounds. "LSCache vs Drupal's dynamic_page_cache UNCACHEABLE hint" covers the case where the two cacheability signals disagree, documents how to opt out via response policy when needed. - 5593f00: ROADMAP records the rc2 -> beta3 step-back with the rationale and the deferred-to-1.3.1/1.4.x list (post-purge sentinel verification flag, everything-invalidation type, the operator-experience polish items). Pure operator-experience + docs scope. No API contract changes; the "HTTP 200 = SUCCEEDED" mapping is unchanged. Finding 7 (auth- served-as-anon variant) was canary-clean on rc2 across 81 controlled requests; treating that finding as resolved pending the invalidation-driven re-test once LSWS-side PURGE works in the test environment. Soak target ~5-7 days. If clean, promotes straight to rc3, then ~2-week soak, then 1.3.0 stable. If anything new surfaces, beta4 cycle.
-
1.3.0-rc2
f037d9b8 · ·1.3.0-rc2: silent-failure fix on missing purger config Cut after the 1.3.0-rc1 production-soak handback from portal-master surfaced a silent failure mode: when lscache_purger.settings is missing from active config (likely from a partial config-sync round-trip on the operator side), purge_host reads as NULL, the host fallback chain in CLI/cron context collapses to empty, every tag invalidation bails with FAILED, and the only signal is a single watchdog warning that operators don't naturally check when drush just says FAILED. The bug has existed since 1.0.0-alpha2 (purger code unchanged through every release). The original prefix-mismatch hypothesis was ruled out by the env-session diagnostic showing identical TagHeaderBuilder::getPrefix() values on web and CLI. What landed since 1.3.0-rc1 (commit 676931f): - c0d7706: defensive install + backfill update hook on the lscache_purger submodule. ensure_settings_exist() runs in hook_install after the Purge plugin wiring; lscache_purger_ update_8101() catches existing installs in the gap state via drush updb. - 72f613a: status report row for the purger surfaces three states (missing config, empty purge_host, OK). markAllFailed() also surfaces its failure reason via $this->messenger()->addError() so drush invocations show the cause inline instead of only in watchdog. - 1c53f72: README clarification that the lscache.settings:enabled toggle controls header emission only; LSWS-side caching is governed by the .htaccess CacheLookup directive separately. To fully bypass LSWS for a debugging window, comment out the directive rather than just toggling the module off. - 02047cf: regression-guard tests for the missing-host failure path. LscachePurgerTest covers the gap state (no config + no request stack + no $base_url) for both single and batch invalidations. - d65f454, 38a46cc, f037d9b: three CI fix-up commits addressing PHPStan, PHPCS, CSpell, and PHPUnit feedback (messenger via MessengerTrait, missing docblocks, banned-words swap, missed function spacing, plugin id placement). Strict bug-fix only relative to rc1; no API contract changes. The "everything" invalidation type that was discussed for this cycle was deferred to 1.3.1 or 1.4.x to preserve rc API freeze. Operator-experience polish from the rc1 hand-back report (status report directives summary, vary_cookies recent-traffic validation, demo ESI submodule + block-form ESI checkbox + drush list command) all carry over to the polish window after 1.3.0 stable. Finding 7 (auth-served-as-anon variant) from the rc1 hand-back is still outstanding; diagnostic ask queued to send to the env session alongside this rc2 deployment.
-
1.3.0-rc1
676931f1 · ·1.3.0-rc1: API freeze, pre-rc1 audit hardening landed Cut after a methodical pre-rc1 audit on the 1.3.x line. The audit read every PHP, YAML, and Markdown file on the branch, checked against Drupal coding standards, common security anti-patterns, and the SA-team-flag risk surface. One security finding (defence in depth on the ESI fragment route), two coding-standards items, two doc refreshes, three test-coverage gaps. All seven landed in the four "rc1 prep" commits since 1.3.0-beta2. The version bump signals API freeze for the 1.3.x track and asks operators to soak the module on production-shape installs ahead of 1.3.0 stable. Per the 1.0.0 trajectory, rc1 -> ~2 weeks soak -> 1.3.0 stable. What landed since 1.3.0-beta2 (commit 02e98fd): - B1 / 70c388e: TrustedCallbackInterface enforcement on ESI fragment callbacks. The fragment route previously gated callable invocation on HMAC token verification only; now also checks that the resolved class declares itself trusted via Drupal core's standard marker interface, mirroring the #lazy_builder policy. Defence in depth: bounds the impact of a hash-salt compromise. No exploitable bug; SA-team-aligned hardening. - S1+S2 / 4516f72: LscachePurgerSettingsForm constructor params now declare types (ConfigFactoryInterface, TypedConfigManager Interface). TagHeaderBuilder::resolvePrefix() removed entirely as it accepted a parameter it never used and just delegated to getPrefix(). - S6+S7 / 480889f: lscache.install status report description updated to link at the 1.3.x docs branch instead of 1.0.x. htaccess-gotchas.md gained version notes for 1.0.0-rc1, 1.0.0, 1.0.1, 1.1.x, 1.2.x, 1.3.x covering the additional .htaccess and LSWS-side directives each release introduces, plus a summary table mapping feature surface to required directives. - S3+S4+S5 / 676931f: three new unit-test classes covering Esi::preRenderEsi's missing-callback path, the fragment controller's TrustedCallbackInterface enforcement (positive and negative cases for both static-method and service-method callable shapes), and the install hook's flatten-defaults helper (six cases covering scalars, mappings, sequences, empty arrays, deep nesting, and mixed shapes). What 1.3.x cumulatively ships: - 1.0.x: cache-tag headers, Purge framework integration, status report .htaccess check, purge_host scheme validation - 1.1.x: per-user private cache mode driven by Drupal cache contexts, admin-route skip, log placeholder substitution - 1.2.x: ESI render element with HMAC-signed fragment URLs and TrustedCallbackInterface enforcement on the resolved callback - 1.3.x: named vary cookies that emit X-LiteSpeed-Vary header for every configured cookie on every cacheable response Stability commitment carried forward from beta2 unchanged: the config schema, response-header contract, render-element shape, fragment-route URL pattern, and TagHeaderBuilder service are all stable from this release. Path to 1.3.0 stable is bug-fix only.
-
1.3.0-beta2
02e98fd9 · ·1.3.0-beta2: form WSOD fix + config backfill hook + branch consolidation Two bug fixes from the 1.3.0-beta1 production-soak handback. The soak halted at Day 0 because neither bug let any traffic reach the install. Fix 1 (critical, form WSOD): the ConfigTarget closures for private_cache.contexts and vary_cookies declared strict array parameter types. Config::get() returns NULL when a key is absent; the strict typing threw a TypeError inside the closure and took out the entire settings page. Both closures now declare ?array and fall back to an empty list. Fix 2 (high, config backfill): new lscache_update_8103 walks the install YAML and writes any missing default keys to active config without overwriting operator changes. Generic implementation that backfills any missing key from config/install/lscache.settings.yml, so future schema additions pick up the same behaviour automatically. Branch consolidation: from 1.3.0-beta2 forward, all LSCache development happens on 1.3.x. The 1.1.x and 1.2.x branches are frozen at their respective beta1 tags. Operators previously on those should bump to 1.3.0-beta2 to pick up the fixes; 1.3.x's cumulative branch model means every prior feature is preserved.
-
1.3.0-beta1
60d5a33c · ·1.3.0-beta1: API freeze for the 1.3.x vary-cookies track Cut after alpha3 came back from a clean re-test on portal-dev with both alpha2 bugs verified fixed: - ESI fragments render and invalidate via cache tags (alpha2 bug fixed by the Markup::create wrap forward-ported from 1.2.0-alpha3). - vary_cookies emits X-LiteSpeed-Vary: cookie=NAME on every cacheable response (alpha2 bug fixed by dropping the cache-context-required emission logic). - No regressions on inherited 1.1 admin-route + cache-poisoning checks. Same code as 1.3.0-alpha3 plus a README addition documenting the matching LSWS server-side CacheVary directive. The reporter caught this as a doc-level gap during the re-test - header was emitting correctly but LSWS didn't honour it without the server-side hint. What 1.3 ships: named vary-cookie support on top of 1.2.x's ESI work and 1.1.x's private cache. Operators configure cookie names in the settings form; the response subscriber emits the vary header for every configured cookie on every cacheable response. Cumulative: 1.3.0-beta1 contains all 1.0.x stable code + 1.1.x private cache + 1.2.x ESI + 1.3.x vary cookies. Operators wanting to validate the full 1.x line on one install can soak this build. Stability commitment: vary_cookies config schema, the X-LiteSpeed-Vary emission contract, and the matching CacheVary directive expectation are stable from this release. Path to 1.3.0 stable is bug-fix only. Known issue carried forward, not blocking beta1: post-PURGE re-cache flake on responses without ESI markup, observed by the adopter during alpha3 re-test. Couldn't be isolated; flagged for further investigation.
-
1.2.0-beta1
5c5cb51b · ·1.2.0-beta1: API freeze for the 1.2.x ESI track Cut after alpha3 came back from a clean re-test on portal-dev with the alpha2 ESI bug verified fixed: - Token-tampering rejection clean across four variants (mutated args, mutated signature, swapped callback, garbage token). - Cache-tag invalidation refreshes ESI fragments correctly. - No regressions on inherited 1.1 admin-route + cache-poisoning checks. Same code as 1.2.0-alpha3 plus a ROADMAP docs bump. The version bump signals API freeze for the 1.2.x track and asks operators to soak the module on production-shape sites ahead of rc1. What 1.2 ships: an ESI render element ('lscache_esi') that emits <esi:include> tags for per-user fragments, plus the fragment route, HMAC-SHA256 token signer, and lscache_esi cache tag for site-wide invalidation. Builds on 1.1's private-cache machinery. Stability commitment: the 'lscache_esi' element shape, the fragment-route URL pattern, the token format, and the LscacheTokenSigner service are stable from this release. Path to 1.2.0 stable is bug-fix only. -
1.3.0-alpha3
05f439a7 · ·1.3.0-alpha3: ESI Markup wrap + vary-cookies always-emit-when-configured Two bug fixes from the 1.3.0-alpha2 adopter field test on portal-dev. Fix 1 (forward-port from 1.2.0-alpha3): ESI render element wraps its treats the emitted <esi:include> tag as already-trusted. Without the wrap, Xss::filterAdmin stripped the tag entirely and ESI fragments produced no output. Same wrap applied to the missing-callback fallback marker. Fix 2: vary-cookie emission no longer requires the response to declare a matching cookies:NAME cache context. Alpha2's logic intersected the configured cookie list with the response's cache contexts, which silently dropped the header on any response whose render code didn't happen to declare the context — meaning the feature was inert for most installs. Alpha3 emits X-LiteSpeed-Vary: cookie=A,B,C for every configured cookie on every cacheable response. LSWS handles cookies that are absent from a given request as "no variance" cleanly. The operator's config IS the directive. Existing vary-cookie unit tests updated to match the new always-emit behaviour. Empty-config default still emits no header, so installs that don't configure vary cookies see no behaviour change.
-
1.2.0-alpha3
9cf2d33f · ·1.2.0-alpha3: wrap ESI #markup with Markup::create to bypass XSS filter Critical bug fix from the 1.3.0-alpha2 adopter field test on portal-dev. ESI fragments produced no output at all in 1.2.0-alpha2 because the renderer pipes #markup strings through Xss::filterAdmin(), which only whitelists a fixed set of HTML tags. <esi:include> is not on that list, so the entire string was being stripped to empty before output. Same outcome for the HTML-comment fallback marker. The fix is to wrap the emitted markup in \Drupal\Core\Render\Markup, which marks a string as already-trusted output and bypasses the filter. The src URL is separately escaped via htmlspecialchars before the wrap, so marking the resulting string as safe is correct.
-
1.3.0-alpha2
4c98bc44 · ·1.3.0-alpha2: forward-port of 1.1.0-alpha2 fixes onto vary-cookie baseline First public alpha of the 1.3.x track. Same vary-cookie response subscriber additions and config schema as the never-tagged 1.3.0-alpha1, plus the two response-subscriber correctness fixes from 1.1.0-alpha2 brought forward via merge of 1.2.x (which itself merged 1.1.x): - Admin routes no longer emit any LSCache header. - Debug log messages contain resolved values inline. 1.3.0-alpha1 was implemented and CI-validated but never tagged or released, since the alpha1 field-test loop ran on 1.1.x first. 1.3.0-alpha2 is the first public alpha of the 1.3.x track.
-
1.2.0-alpha2
0fe4eea6 · ·1.2.0-alpha2: forward-port of 1.1.0-alpha2 fixes onto ESI baseline First public alpha of the 1.2.x track. Same ESI render element and fragment-route work as the unreleased 1.2.0-alpha1, plus the two response-subscriber correctness fixes from 1.1.0-alpha2 brought forward via merge of 1.1.x: - Admin routes no longer emit any LSCache header (AdminContext- based skip, covers /admin/* plus contrib routes flagged _admin_route: TRUE). - Debug log messages contain resolved values rather than literal @placeholder text (inline sprintf substitution, works in dblog UI and drush watchdog:show alike). The 1.2.0-alpha1 tag was cut but never published as a release node, since the alpha1 field-test loop ran on 1.1.x first and surfaced bugs that needed to land before any 1.2 alpha hit operators.
-
1.1.0-beta1
489d3504 · ·1.1.0-beta1: API freeze for the 1.1.x track Cut after alpha2 came back from a clean re-test on portal-dev with both alpha1 findings verified fixed and zero regressions. Same code as 1.1.0-alpha2 (commit 1322537) plus a ROADMAP docs bump. The version bump signals API freeze for the 1.1.x track and asks operators to soak the module on production-shape installs ahead of rc1. What 1.1 ships: per-user private-cache mode for authenticated Drupal users. Responses whose Drupal cache contexts indicate per-user variation get X-LiteSpeed-Cache-Control: private,max-age=N automatically; operators do not hand-tag routes or fragments. Field-test history through alpha cycle: - Cache-poisoning canary clean across 18 trials. - Anonymous response back-compat byte-identical to 1.0.0-rc1. - Two bugs found and fixed in alpha2: admin-route over-emission, drush watchdog:show placeholder rendering. - Status report row dynamic against the .htaccess directive state. - Update hook lscache_update_8101 idempotent across repeats. Stability commitment: response-header shape, private_cache.* config schema, administer lscache permission, and TagHeaderBuilder service are stable from this release. Path to 1.1.0 stable is bug-fix only.
-
1.1.0-alpha2
1322537e · ·1.1.0-alpha2: skip admin routes; substitute log placeholders inline Two bug fixes from the alpha1 field test on portal-dev. The cache- poisoning canary (3 cycles x 3 page shapes x 2 users = 18 trials) passed cleanly on alpha1, validating the architecture; what surfaced was scope-creep on the private-cache emission rather than a correctness flaw. Fix 1: admin routes no longer emit any LSCache header. Alpha1's decision tree only checked Cache-Control: no-store to disqualify responses, but admin pages return must-revalidate, no-cache, private (same as authenticated content) and so were being held in private cache for the full TTL. Practical impact was bounded by per-user keying (no cross-user leakage) but settings forms looked stale to operators for up to 600s after a change. Switched to AdminContext ::isAdminRoute() for the skip - covers everything under /admin/* plus contrib routes flagged _admin_route: TRUE. Fix 2: debug log messages now contain resolved values rather than literal @tags / @ttl placeholder text. Drupal's dblog UI substitutes the placeholders correctly, but drush watchdog:show doesn't run the same pipeline. Alpha1 used the deferred-substitution pattern; alpha2 uses inline sprintf strings so every log consumer sees resolved values.
-
1.2.0-alpha1
98f2967c · ·1.2.0-alpha1: ESI render element on top of 1.1's private cache First alpha of the 1.2.x track. Adds a new render element type 'lscache_esi' that emits an <esi:include src="..." /> tag pointing at a fragment route, plus the fragment route itself, plus an HMAC-SHA256 token signer that gates fragment access. Drupal-native: 'lscache_esi' accepts the same #callback and #args shape as #lazy_builder. A render-array author who already knows how to write a lazy builder can promote it to an ESI fragment with one key change. The surrounding response stays public-cacheable because the per-user variation is pushed out to the fragment, and the fragment itself emits private-cache headers picked up by 1.1's response subscriber. Token format is base64url(json-payload).hex(hmac-sha256), keyed on the site's hash salt. Tampering, replaying, or signing with a different salt all fail verification cleanly. Every fragment also carries an 'lscache_esi' cache tag so a single invalidation drains every fragment site-wide.
-
1.1.0-alpha1
de8258b4 · ·1.1.0-alpha1: private-cache mode for authenticated users First alpha of the 1.1.x track. Extends caching from public-only to LSCache's per-user private cache. Authenticated user pages, which 1.0.x passed through to PHP, can now be held per-user. Drupal-native: the response subscriber detects per-user cache contexts (user, user.permissions, user.roles, session, configurable) and emits X-LiteSpeed-Cache-Control: private,max-age=N automatically. Operators do not hand-tag routes or fragments. Off by default; private cache scales with active users. Default TTL 600s (vs 3600s public default). Status report row warns if private mode is enabled but .htaccess lacks 'CacheLookup ... private on'. Sites upgrading from 1.0.x get the new defaults via lscache_update_8101. Roadmap order updated: 1.1 is now private cache, 1.2 is ESI. ESI fragments are private-cached responses at their own URLs, so private cache is the foundational primitive ESI builds on.
-
1.0.1
aa5d16b2 · ·1.0.1 hardening release Adds element-level validation that restricts the LSCache Purger purge_host field to http:// and https:// schemes. Drupal's url element type otherwise accepts any scheme PHP's filter_var recognises (file, ftp, ldap, gopher, etc.). The HTTP purger only makes sense over http or https; other schemes would have failed at runtime anyway, but the new validator surfaces an immediate error and closes a (very narrow) avenue for an admin with the 'administer lscache' permission to use the form to probe internal endpoints. Operator-facing only; permission-gated. No security advisory associated as no exploitable vulnerability was identified. Defence in depth from the 1.0.0 pre-stable security review.
-
1.0.0
3f375e86 · ·1.0.0 stable First stable release of the Drupal.org-native LSCache integration. Cut after rc1 soaked cleanly across TubeSpanner Portal and several live production sites for ten days with no tag-invalidation regressions, header correctness issues, or open queue reports. Same code as rc1 plus a cosmetic info.yml description polish. 1.0.0 signals API stability and opens the project to Drupal Security Advisory coverage. 1.0.x is now bug-fix and security-patch only; new feature work moves to 1.1.x.
-
1.0.0-beta2
90346881 · ·