includes/Cache/class-cache.php
Cache class for handling caching functionalities in PerformanceOptimise plugin.
Class Cache
Class Cache
class CacheConstants
| Constant | Visibility | Value | Line |
|---|---|---|---|
CACHE_DIR | private | \'/cache/wppo\' | 38 |
Properties
| Property | Visibility | Type | Default | Line |
|---|---|---|---|---|
$domain | private | string | — | 88 |
$host_mismatch | private | bool | false | 100 |
$cache_root_dir | private | string | — | 108 |
$cache_root_url | private | string | — | 116 |
$url_path | private | string | — | 124 |
$path_rejected | private | bool | false | 139 |
$inline_drift_detected | private | bool | false | 152 |
$buffer_enhanced | private static | bool | false | 167 |
$inline_drift_logged | private static | bool | false | 180 |
$inline_size_map | private | ?array | null | 199 |
$core_will_inline_memo | private | array | array() | 211 |
$src_stat_cache | private | array | array() | 219 |
$filesystem | private | object|false|null | null | 227 |
$cache_ob_level | private | ?int | null | 239 |
$fs_initialized | private | bool | false | 247 |
$request_uri | private | string | — | 255 |
$options | private | array | array() | 263 |
$image_optimisation | private | ?Image_Optimisation | null | 271 |
$google_fonts | private | ?Google_Fonts | null | 279 |
$current_role_hash | private | string | \'\' | 287 |
$no_cache_marker_written | private | bool | false | 297 |
$combine_css_preload_url | private | string | \'\' | 306 |
$css_combine | private | ?Css_Combine | null | 318 |
$cache_invalidator | private | ?Cache_Invalidator | null | 331 |
$cache_capacity | private | ?Cache_Capacity | null | 343 |
$sandbox_effective_file_opt_memo | private | array | array() | 1000 |
$sandbox_preview_memo | private | bool|null | null | 1014 |
$safe_mode_inline_memo | private | array<string,bool> | array() | 1026 |
publicstatic bump_stats_cache()
public static function bump_stats_cache(): voidInvalidate the cached dashboard stats.
Return: void.
public __construct()
public function __construct(array=array() $options, ?Image_Optimisation=null $image_optimisation, ?Google_Fonts=null $google_fonts)Constructor to initialize cache settings and configurations. Parameter Type Default Description $optionsarray=array()— Plugin options. Callers pass Util::get_settings(); when empty, loaded from DB via Util::get_settings() for backward compatibility. $image_optimisation?Image_Optimisation=null— Live image-optimisation collaborator (identity shared with Main), or null to build lazily from the resolved options. $google_fonts?Google_Fonts=null— Live Google-Fonts collaborator (identity shared with Main), or null to build lazily from the resolved options.
public is_host_mismatched()
public function is_host_mismatched(): boolWhether the request Host header mismatched the canonical home host.
Return: bool — True when the request host differs from the canonical host.
public host_mismatch()
public function host_mismatch(): boolWhether the request Host header mismatched the canonical home host.
Return: bool — True when the request host differs from the canonical host.
public cache_key()
public function cache_key(): stringCanonical, query-normalized cache key for the current request.
Return: string — `{canonical-host}/{path}` (homepage: `{host}/`), or \’\’ when refused.
private is_cache_allowed_for_current_user()
private function is_cache_allowed_for_current_user(): boolCheck whether page caching is allowed for the current (possibly logged-in) user.
Return: bool — True if the current user may receive a cached page.
private get_logged_in_role_hash()
private function get_logged_in_role_hash(): stringCompute a stable 12-char hex hash of the current user\’s sorted roles.
Return: string — Role hash or empty string.
public set_image_optimisation()
public function set_image_optimisation(Image_Optimisation $image_optimisation): voidSet the Image_Optimisation instance to reuse instead of creating a new one. Parameter Type Default Description $image_optimisationImage_Optimisation— The existing instance.
Return: void.
public set_google_fonts()
public function set_google_fonts(Google_Fonts $google_fonts): voidSet the Google_Fonts instance to reuse instead of creating a new one. Parameter Type Default Description $google_fontsGoogle_Fonts— The existing instance.
Return: void.
private get_image_optimisation()
private function get_image_optimisation(): Image_OptimisationLazily resolve the Image_Optimisation buffer collaborator.
Return: Image_Optimisation — Non-null collaborator.
private get_google_fonts()
private function get_google_fonts(): Google_FontsLazily resolve the Google_Fonts buffer collaborator.
Return: Google_Fonts — Non-null collaborator.
private get_filesystem()
private function get_filesystem(): object|false|nullLazily initializes and returns the WP_Filesystem object.
Return: object|null — The filesystem object, or null before the first initialization attempt.
private block_assets_are_separate()
private function block_assets_are_separate(): boolWhether WP 6.9+ core is loading separate (on-demand) core block assets.
Return: bool — True when core loads separate core block assets on demand.
private is_core_block_asset()
private function is_core_block_asset($handle, bool $separate_block_assets): boolWhether a style handle belongs to the core block-assets family. Parameter Type Default Description $handlestring— The registered style handle. $separate_block_assetsbool— Whether core loads separate block assets.
Return: bool — True if the handle is a core block asset under separate assets.
private is_combined_core_block_monolith_forced()
private function is_combined_core_block_monolith_forced(): boolWhether the operator forced the combined core block-assets monolith.
Return: bool — True when the combined monolith is forced.
private get_effective_separate_block_assets()
private function get_effective_separate_block_assets(): boolEffective separate-assets state after the monolith escape hatch.
Return: bool — True when core owns on-demand block styles on this request.
private classic_block_assets_on_demand_active()
private function classic_block_assets_on_demand_active(): boolWhether WP 6.8 classic on-demand block-asset loading is active.
Return: bool — True when classic on-demand block assets are active.
private is_classic_on_demand_block_asset()
private function is_classic_on_demand_block_asset($handle): boolWhether a handle is a classic (pre-6.9) on-demand block asset. Parameter Type Default Description $handlestring— The registered style handle.
Return: bool — True when the handle must stay out of the combined file.
private is_core_per_block_style_handle()
private function is_core_per_block_style_handle($handle): boolWhether a queued style handle is a verifiable core per-block asset. Parameter Type Default Description $handlestring— Queued style handle e.g. \’wp-block-cover\’.
Return: bool — True when the handle is verifiably a core per-block asset.
private is_hidden_block_asset_for_combine()
private function is_hidden_block_asset_for_combine($handle): boolWhether a handle is a hidden block asset that must stay out of the combine. Parameter Type Default Description $handlestring— The registered style handle.
Return: bool — True when the handle is hidden and must not be combined.
private is_duplicate_of_core_output()
private function is_duplicate_of_core_output($handle, bool $separate_block_assets): boolWhether a handle is already owned by core output and must never be combined. Parameter Type Default Description $handlestring— The registered style handle. $separate_block_assetsbool— Whether core loads separate block assets.
Return: bool — True when the handle must stay out of the combined file.
private should_bypass_for_litespeed()
private function should_bypass_for_litespeed(): boolWhether WPPO optimisers should be bypassed for LiteSpeed co-existence.
Return: bool — True if the current request should bypass WPPO optimisation.
private get_sandbox_effective_file_opt()
private function get_sandbox_effective_file_opt(array $file_opt): arraySandbox-effective `file_optimisation` slice (issue #1259). Parameter Type Default Description $file_optarray— Production `file_optimisation` slice.
Return: array — Effective slice (staged values merged in preview). Facade proxy (ARCH-006): logic lives in {@see Css_Combine::get_sandbox_effective_file_opt}.
private should_bypass_combine_for_elementor()
private function should_bypass_combine_for_elementor(array $file_opt, ?bool=null $looks_like): boolWhether CSS combine/inline must be skipped for Elementor-safe mode (issue #1259). Parameter Type Default Description $file_optarray— Sandbox-effective `file_optimisation` slice. $looks_like?bool=null— Pre-computed Main::looks_like_elementor_request() verdict (null to compute here). Threaded through so will_combine_css_inline() evaluates the pre-gate once instead of twice per request.
Return: bool — True when combine/inline must be skipped. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::should_bypass_combine_for_elementor}.
private css_combine()
private function css_combine(): Css_CombineLazily-created CSS-combine runner (ARCH-006).
Return: Css_Combine — Combine runner bound to this instance.
private invalidator()
private function invalidator(): Cache_InvalidatorLazily-created invalidation/purge runner (ARCH-007).
Return: Cache_Invalidator — Invalidator bound to this instance.
private capacity()
private function capacity(): Cache_CapacityLazily-created capacity/accounting runner (P3-005).
Return: Cache_Capacity — Capacity owner bound to this instance.
public combine_css()
public function combine_css()Combines all enqueued CSS files into a single file.
Return: void.
private resolve_eligible_handles()
private function resolve_eligible_handles(array $styles, array $exclusions): arrayResolve the handles eligible for CSS combining. Parameter Type Default Description $stylesarray— Queued handles. $exclusionsarray— Excluded handles/patterns.
Return: array — Eligible handles. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::resolve_eligible_handles}.
private fetch_and_minify_css()
private function fetch_and_minify_css(array $eligible_handles): arrayFetch, concatenate and minify eligible stylesheets. Parameter Type Default Description $eligible_handlesarray— Eligible handles.
Return: array{css:string,handles:array,error:string} — Combined CSS + successful handles + error stage (\’\’ on success). Facade proxy (ARCH-006): logic lives in {@see Css_Combine::fetch_and_minify_css}.
private write_combined_file()
private function write_combined_file(string $combined_css, string $css_variant): arrayWrite the combined CSS file to the cache directory. Parameter Type Default Description $combined_cssstring— Combined CSS. $css_variantstring— Cache variant suffix.
Return: array{path:string,error:string} — File path (\’\’ on failure) + error stage. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::write_combined_file}.
private emit_combined_preload_hint()
private function emit_combined_preload_hint($css_url, $version, string $css_file_path): voidEmit the preload hint for the combined stylesheet. Parameter Type Default Description $css_urlstring— URL of the combined stylesheet. $versionint|string— Cache-busting version suffix. $css_file_pathstring— Absolute path to the combined CSS file.
Return: void — Facade proxy (ARCH-006): logic lives in {@see Css_Combine::emit_combined_preload_hint}.
private set_combine_css_preload()
private function set_combine_css_preload($css_url, $version, $css_file_path): voidRecord the combined-CSS preload URL for emission on `wp_head`. Parameter Type Default Description $css_urlstring— URL of the combined stylesheet. $versionint|string— Cache-busting version suffix. $css_file_pathstring— Absolute path to the combined CSS file.
Return: void.
public maybe_preload_combine_css()
public function maybe_preload_combine_css(): voidEmit the combined-CSS `rel=\”preload\”` resource hint on `wp_head`.
Return: void.
private is_excluded_from_combine()
private function is_excluded_from_combine($handle, $src, array $exclude_combine_css): boolWhether a handle should be excluded from CSS combining. Parameter Type Default Description $handlestring— Handle to check. $srcstring— Style src URL. $exclude_combine_cssarray— Exclusion list.
Return: bool — True if excluded. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::is_excluded_from_combine}.
private get_combined_handles()
private function get_combined_handles($styles, $exclude_combine_css): arrayComputes the set of handles that belong in the combined CSS file. Parameter Type Default Description $stylesarray— The enqueued style handles. $exclude_combine_cssarray— Handles/URL fragments excluded from combining.
Return: array — The handles that would be combined. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::get_combined_handles}.
private combined_handles_match()
private function combined_handles_match($css_file_path, array $eligible_handles): boolWhether a cached combined file was generated from the same handle set. Parameter Type Default Description $css_file_pathstring— Absolute path to the combined CSS file. $eligible_handlesarray— The handles expected in the combined file.
Return: bool — True if the cached file matches the current handle set. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::combined_handles_match}.
private write_combined_handles()
private function write_combined_handles($css_file_path, array $eligible_handles): voidPersists the set of combined handles next to the combined CSS file. Parameter Type Default Description $css_file_pathstring— Absolute path to the combined CSS file. $eligible_handlesarray— The handles combined into the file.
Return: void — Facade proxy (ARCH-006): logic lives in {@see Css_Combine::write_combined_handles}.
private core_will_inline()
private function core_will_inline($handle): boolWhether a queued style will be inlined by core instead of combined. Parameter Type Default Description $handlestring— The registered style handle.
Return: bool — True if core will inline the style, false otherwise. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::core_will_inline}.
private core_inline_budget_will_inline()
private function core_inline_budget_will_inline($handle, $limit, $core_faithful): boolSimulates core\’s greedy smallest-first inline-CSS budget for a handle. Parameter Type Default Description $handlestring— The registered style handle. $limitint— The inline size limit in bytes. $core_faithfulbool— Whether to mirror core\’s candidate collection.
Return: bool — True if core\’s budget pass would inline the style. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::core_inline_budget_will_inline}.
private log_inline_budget_drift()
private function log_inline_budget_drift($handle, $limit): voidLogs that the inline-CSS budget prediction drifted from core. Parameter Type Default Description $handlestring— The handle whose prediction drifted. $limitint— The inline size limit in bytes.
Return: void — Facade proxy (ARCH-006): logic lives in {@see Css_Combine::log_inline_budget_drift}.
private is_safe_css_combine_fallback_enabled()
private function is_safe_css_combine_fallback_enabled(): boolWhether the safe CSS combine fallback is enabled.
Return: bool — True when fallback guards are active. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::is_safe_css_combine_fallback_enabled}.
private is_combined_css_valid()
private function is_combined_css_valid(string $path): boolWhether a combined CSS file is valid (exists, readable, non-empty). Parameter Type Default Description $pathstring— Absolute path to the combined CSS file.
Return: bool — True when the file is usable. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::is_combined_css_valid}.
private log_combine_fallback()
private function log_combine_fallback(string $reason, array $handles): voidLog a combine fallback (fail-open) event with throttling. Parameter Type Default Description $reasonstring— Machine-readable reason (empty_payload, fetch_failure, write_failure, head_match_failure). $handlesarray— Handles preserved by the fallback.
Return: void — Facade proxy (ARCH-006): logic lives in {@see Css_Combine::log_combine_fallback}.
private register_combine_css_path()
private function register_combine_css_path($css_file_path): voidRegisters the combined CSS file with `path` data for core\’s inline pass. Parameter Type Default Description $css_file_pathstring— Absolute path to the combined CSS file.
Return: void — Facade proxy (ARCH-006): logic lives in {@see Css_Combine::register_combine_css_path}.
private should_bypass_inline_for_safe_mode()
private function should_bypass_inline_for_safe_mode(array $file_opt): boolWhether the combined CSS inline must be skipped for safe mode (issue #1465). Parameter Type Default Description $file_optarray— Production `file_optimisation` slice.
Return: bool — True when inlining must be skipped. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::should_bypass_inline_for_safe_mode}.
private will_combine_css_inline()
private function will_combine_css_inline($css_file_path): boolWhether the combined CSS file will be inlined by core. Parameter Type Default Description $css_file_pathstring— Absolute path to the combined CSS file.
Return: bool — True if core will inline the combined file, false otherwise. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::will_combine_css_inline}.
private get_styles_inline_limit()
private function get_styles_inline_limit(): intReads the core `styles_inline_size_limit` budget.
Return: int — The inline size limit in bytes. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::get_styles_inline_limit}.
private inline_candidates_require_src()
private function inline_candidates_require_src(): boolWhether inline candidates must carry a `src` on this core version.
Return: bool — True when inline candidates must carry a `src`. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::inline_candidates_require_src}.
private inline_candidates_require_readable()
private function inline_candidates_require_readable(): boolWhether inline candidates must be readable on this core version.
Return: bool — True when the core-faithful pass must skip unreadable styles. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::inline_candidates_require_readable}.
private get_cached_src_stat()
private function get_cached_src_stat(string $path): arrayRetrieve cached src file stat (readable + filesize) with per-request LRU. Parameter Type Default Description $pathstring— Absolute filesystem path.
Return: array{readable:bool,size:int|false} — Facade proxy (ARCH-006): logic lives in {@see Css_Combine::get_cached_src_stat}.
private should_skip_combine_for_inline_budget()
private function should_skip_combine_for_inline_budget(array $eligible_handles): boolWhether the combined-CSS file should be skipped on small block-theme bundles. Parameter Type Default Description $eligible_handlesarray— Handles that would be combined.
Return: bool — True when combining should be skipped. Facade proxy (ARCH-006): logic lives in {@see Css_Combine::should_skip_combine_for_inline_budget}.
private measure_style_byte_size()
private function measure_style_byte_size($handle)Measure a style\’s byte size using core\’s inline-budget accounting. Parameter Type Default Description $handlestring— The registered style handle.
Return: int|false — Byte size, 0 when the handle contributes nothing, false when unmeasurable.
private fetch_remote_css()
private function fetch_remote_css($url)Fetches CSS content from a remote URL or local path. Parameter Type Default Description $urlstring— The URL of the CSS file.
Return: string|false — The CSS content or false if fetching fails.
public combine_options()
public function combine_options(): arrayPlugin options snapshot for the CSS-combine service (ARCH-006 internal bridge).
Return: array — Plugin options snapshot.
public combine_filesystem()
public function combine_filesystem(): object|false|nullFilesystem for the CSS-combine service (ARCH-006 internal bridge).
Return: object|false|null — The filesystem object, or false on init failure.
public combine_cache_file_path()
public function combine_cache_file_path(string $variant): stringCombined-CSS file path for the CSS-combine service (ARCH-006 internal bridge). Parameter Type Default Description $variantstring— Cache variant suffix.
Return: string — Absolute path to the combined CSS file.
public combine_prepare_cache_dir()
public function combine_prepare_cache_dir(): boolCache-directory preparation for the CSS-combine service (ARCH-006 internal bridge).
Return: bool — True when the cache directory is writable.
public combine_save_css()
public function combine_save_css(string $css, string $path): voidPersist the combined CSS for the CSS-combine service (ARCH-006 internal bridge). Parameter Type Default Description $cssstring— Combined CSS payload. $pathstring— Absolute path to the combined CSS file.
Return: void.
public combine_should_bypass_for_litespeed()
public function combine_should_bypass_for_litespeed(): boolLiteSpeed bypass verdict for the CSS-combine service (ARCH-006 internal bridge).
Return: bool — True when the current request must bypass WPPO optimisation.
public combine_is_cache_allowed_for_current_user()
public function combine_is_cache_allowed_for_current_user(): boolLogged-in cache gate for the CSS-combine service (ARCH-006 internal bridge).
Return: bool — True when page output may be cached for the current user.
public combine_is_not_cacheable()
public function combine_is_not_cacheable(): boolCacheability gate for the CSS-combine service (ARCH-006 internal bridge).
Return: bool — True when the current request must not be cached.
public combine_get_effective_separate_block_assets()
public function combine_get_effective_separate_block_assets(): boolEffective separate block-assets state for the CSS-combine service (ARCH-006 internal bridge).
Return: bool — True when core owns on-demand block styles on this request.
public combine_is_duplicate_of_core_output()
public function combine_is_duplicate_of_core_output(string $handle, bool $separate_block_assets): boolCore-output dedupe verdict for the CSS-combine service (ARCH-006 internal bridge). Parameter Type Default Description $handlestring— The registered style handle. $separate_block_assetsbool— Whether core loads separate block assets.
Return: bool — True when the handle must stay out of the combined file.
public combine_is_throttled()
public function combine_is_throttled(string $throttle_key, int $ttl): boolLog throttle for the CSS-combine service (ARCH-006 internal bridge). Parameter Type Default Description $throttle_keystring— Transient key guarding the throttle window. $ttlint— Throttle window in seconds.
Return: bool — True when the key was already seen inside the window.
public combine_inline_drift_detected()
public function combine_inline_drift_detected(): boolInline-drift flag read for the CSS-combine service (ARCH-006 internal bridge).
Return: bool — True when the inline-budget prediction drifted from core this request.
publicstatic combine_inline_drift_already_logged()
public static function combine_inline_drift_already_logged(): boolDrift-log process gate read for the CSS-combine service (ARCH-006 internal bridge).
Return: bool — True when the drift notice was already logged this PHP process.
publicstatic combine_mark_inline_drift_logged()
public static function combine_mark_inline_drift_logged(): voidDrift-log process gate write for the CSS-combine service (ARCH-006 internal bridge).
Return: void.
publicabstract ()
public abstract function (public combine_state_preload_url():string{return->combine_css_preload_url;}/** * Direct reference to the inline-budget size map (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return array<string,array{size:int,readable:bool}>|null Reference to the live size-map state. */function&combine_state_inline_size_map():?array{return->inline_size_map;}/** * Direct reference to the core-will-inline memo (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return array<string,bool> Reference to the live will-inline memo. */function&combine_state_core_will_inline_memo():array{return->core_will_inline_memo;}/** * Direct reference to the src stat LRU (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return array<string,array{readable:bool,size:int|false}> Reference to the live stat cache. */function&combine_state_src_stat_cache():array{return->src_stat_cache;}/** * Direct reference to the sandbox-effective slice memo (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Per-request lifetime keyed by the * production slice hash, so staged preview output and production * output never share a verdict. Audit note: only `Css_Combine` calls * this (no other runtime or test caller exists). Do not call from new * code; the public visibility exists solely for the extraction bridge. * * @internal * @since 2.4.0 * @return array<string,array> Reference to the live sandbox-slice memo. */function&combine_state_sandbox_effective_file_opt_memo():array{return->sandbox_effective_file_opt_memo;}/** * Direct reference to the sandbox preview-flag memo (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return bool|null Reference to the live preview-flag memo (null = unresolved). */function&combine_state_sandbox_preview_memo(){return->sandbox_preview_memo;}/** * Direct reference to the safe-mode inline-bypass memo (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Per-request lifetime keyed by the * production slice hash. Audit note: only `Css_Combine` calls this * (no other runtime or test caller exists). Do not call from new code; * the public visibility exists solely for the extraction bridge. * * @internal * @since 2.4.0 * @return array<string,bool> Reference to the live safe-mode memo. */function&combine_state_safe_mode_inline_memo():array{return->safe_mode_inline_memo;}/** * Direct reference to the inline-drift flag (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return bool Reference to the live drift flag. */function&combine_state_inline_drift_detected():bool{return->inline_drift_detected;}/** * Filesystem for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return object|false|null The filesystem object, or false on init failure. */functioninvalidator_filesystem():object|false|null{return->get_filesystem();}/** * Path-containment verdict for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @param string $path Absolute file or directory path. * @return bool True when contained. */functioninvalidator_is_path_contained(string):bool{return->is_path_contained();}/** * Cache file path for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @param string|null $url_path The URL path (optional). * @param string $type The file type (default: \'html\'). * @return string The file path. */functioninvalidator_get_file_path(?string=null,string=\'html\'):string{return->get_file_path(,);}/** * Traversal-probe log for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @param string $raw_input The hostile input that was rejected. * @return void */functioninvalidator_log_traversal_probe(string):void{->log_traversal_probe();}/** * Cache root directory for the invalidation/purge service (ARCH-007 internal bridge). * * Direct raw read is intentional: no accessor exists for this property, * so the bridge returns the canonical single-source-of-truth value. * If an accessor is introduced later, route this bridge through it. * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return string Cache root directory. */functioninvalidator_cache_root_dir():string{return->cache_root_dir;}/** * Cache domain for the invalidation/purge service (ARCH-007 internal bridge). * * Direct raw read is intentional: no accessor exists for this property, * so the bridge returns the canonical single-source-of-truth value. * If an accessor is introduced later, route this bridge through it. * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return string Cache domain. */functioninvalidator_domain():string{return->domain;}/** * Cache root URL for the invalidation/purge service (ARCH-007 internal bridge). * * Direct raw read is intentional: no accessor exists for this property, * so the bridge returns the canonical single-source-of-truth value. * If an accessor is introduced later, route this bridge through it. * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return string Cache root URL. */functioninvalidator_cache_root_url():string{return->cache_root_url;}/** * Cache directory slug for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return string Cache directory slug (`CACHE_DIR`). */functioninvalidator_cache_dir():string{returnself::CACHE_DIR;}/** * Filesystem for the capacity/accounting service (P3-005 internal bridge). * * @internal * @access private * @since NEXT * @return object|false|null Filesystem object, or false on init failure. */functioncapacity_filesystem():object|false|null{return->get_filesystem();}/** * Cache root for the capacity/accounting service (P3-005 internal bridge). * * @internal * @access private * @since NEXT * @return string Canonical cache root directory. */functioncapacity_cache_root_dir():string{return->cache_root_dir;}/** * Cache domain for the capacity/accounting service (P3-005 internal bridge). * * @internal * @access private * @since NEXT * @return string Canonical cache domain. */functioncapacity_domain():string{return->domain;}/** * Path-containment verdict for capacity eviction (P3-005 internal bridge). * * @internal * @access private * @since NEXT * @param string $path Absolute file or directory path. * @return bool True when the path is contained. */functioncapacity_is_path_contained(string):bool{return->is_path_contained();}/** * Delete a cache entry through the invalidation owner (P3-005 internal bridge). * * Keeps storage/invalidation policy on Cache_Invalidator while capacity * eviction retains the existing sibling-aware deletion behavior. * * @internal * @access private * @since NEXT * @param string $file_path Cache entry path. * @return bool Whether deletion succeeded. */functioncapacity_delete_cache_files(string):bool{return->invalidator()->delete_cache_files();}/** * Start output buffer for static HTML cache (WP < 6.9 fallback). * * Creates a static HTML version of the page if not logged in and not a 404 page. * * Tracked by #829: do not remove until minimum supported WP is raised * to 6.9 (`Requires at least: 6.9`). * * Buffer lifecycle guarantees (audit #888 finding 6): * - the ob callback is Throwable-safe and always returns a string (the * original buffer on failure), so the page can never lose output and * the buffer can always be closed; * - the opened level is tracked and a shutdown safety net (priority 0, * before core\'s wp_ob_end_flush_all() at priority 1) flushes the * buffer exactly once if nothing else closed it. When a third party * opened a deeper buffer the net stays out of the way — their close * cascades into ours. * * This hook only fires on template_redirect (front-end HTML), never in * REST/AJAX/admin contexts, so buffered REST/JSON responses are not a * concern by construction. * * @return void * * @since 1.0.0 */functionstart_output_buffer():void{=false;try{if(class_exists(\'PerformanceOptimise\\Inc\\Main\')&&method_exists(\'PerformanceOptimise\\Inc\\Main\',\'should_use_core_template_buffer\')){=Main::should_use_core_template_buffer();}else{=function_exists(\'wp_should_output_buffer_template_for_enhancement\');}}catch(\\Throwable){unset();=function_exists(\'wp_should_output_buffer_template_for_enhancement\');}if(){return;}if(function_exists(\'wp_should_output_buffer_template_for_enhancement\')){try{if(wp_should_output_buffer_template_for_enhancement()){return;}}catch(\\Throwable){unset();}}if(!->is_cache_allowed_for_current_user()||->is_not_cacheable()){return;}if(null!==->cache_ob_level){return;}=->get_logged_in_role_hash();=->get_cache_file_path(\'html\',);=function()use(){try{=->process_buffer_only();->save_processed_buffer(,);return;}catch(\\Throwable){do_action(\'wppo_debug_log\',\'WPPO page cache buffer processing failed.\',array(\'exception\'=>));return;}};if(!ob_start()){do_action(\'wppo_debug_log\',\'WPPO page cache could not open output buffer\');return;}->cache_ob_level=ob_get_level();if(false===has_action(\'shutdown\',array(,\'maybe_end_output_buffer\'))){add_action(\'shutdown\',array(,\'maybe_end_output_buffer\'),0);}}/** * Shutdown safety net: close the cache output buffer exactly once. * * Runs at shutdown priority 0, before core\'s wp_ob_end_flush_all() * (priority 1). Acts only while the tracked buffer is still the * topmost-open level: when a third party opened a deeper buffer, their * close (or core\'s shutdown flush) cascades into ours and this method * is a no-op. Also serves the \"is_not_cacheable flipped mid-request\" * case: the save decision is made inside the callback, the (processed) * buffer is always flushed to the client. * * @since 2.0.0 * @return void */functionmaybe_end_output_buffer():void{if(null===->cache_ob_level){return;}=->cache_ob_level;->cache_ob_level=null;try{if(ob_get_level()===){ob_end_flush();}}catch(\\Throwable){unset();}}/** * Hoist late-enqueued block stylesheets after the block library (WP 6.9 core parity). * * Core 6.9 hoists late-enqueued block assets into `<head>` behind the * template-enhancement buffer (Trac #43258; 6.9 frontend-performance * field guide). Because this pipeline now runs ON the core buffer * (issue #1386), hoisted styles are already present: this pass is * the fail-open net for markup where they are not: per-block * stylesheet `<link>` tags still sitting after `</head>` (late * `wp_enqueue_style()` calls printed in body/footer) are moved to * directly after the `block-library` stylesheet link, preserving * their relative order so the core cascade (library first, then * per-block overrides) stays intact. * * HTML API only: discovery walks `WP_HTML_Tag_Processor`; the move * itself uses plain string offsets, never regex. Idempotent (a * second pass finds nothing after `</head>` and no-ops) and * fail-open (any unexpected shape returns the input unchanged). * * @since 2.3.0 * * @param string $buffer The HTML buffer. * @return string The HTML with late block styles hoisted, or the input unchanged. */functionhoist_late_block_styles(){try{if(!is_string()||\'\'===){return;}if(!class_exists(\'WP_HTML_Tag_Processor\')){return;}if(false===stripos(,\'</head\')||false===stripos(,\'block-library\')||(false===stripos(,\'wp-block-\')&&false===stripos(,\'/blocks/\'))){return;}=stripos(,\'</head>\');if(false===){return;}=new\\WP_HTML_Tag_Processor();=array();while(->next_tag(array(\'tag_name\'=>\'LINK\'))){=->get_attribute(\'rel\');if(!is_string()||false===stripos(,\'stylesheet\')){continue;}=->get_attribute(\'href\');if(!is_string()||\'\'===){continue;}[]=;}if(empty()){return;}=null;foreach(as){if(false!==stripos(,\'block-library\')){=;break;}}if(null===){return;}=array();=array();foreach(as){if(===){continue;}if(false!==stripos(,\'block-library\')){continue;}if(false===stripos(,\'wp-block-\')&&false===stripos(,\'/blocks/\')){continue;}if(isset([])||isset([])){continue;}if(->is_href_in_head_link(,,)){[]=true;continue;}[]=true;}if(empty()&&empty()){return;}=->find_link_tag_for_href(,,0);if(null===){return;}=array();=array();=array();=array_merge(array_keys(),array_keys());foreach(as){=isset([]);=;=0;while(<50){++;=->find_link_tag_for_href(,,);if(null===){break;}[]=;if(!&&!isset([])){[]=true;[]=[2];}=[1]+1;}if(count()>100){return;}}if(empty()){return;}usort(,staticfunction(,){if([0]===[0]){return0;}return([0]<[0])?-1:1;});foreach(as){if([0]<=[1]){return;}}=substr(,0,[1]+1);.=implode(\'\',);=[1]+1;foreach(as){if([0]<){continue;}.=substr(,,[0]-);=[1]+1;}.=substr(,);return;}catch(\\Throwable){unset();returnis_string()?:\'\';}}/** * Locate the next `<link>` tag containing an href, at/after an offset. * * String-offset companion to the HTML-API discovery in * {@see hoist_late_block_styles()}: occurrences of the href that are * not wrapped in a `<link>` tag (script strings, data attributes, * comments) are skipped, so callers always resolve to a genuine * stylesheet tag. Never uses regex. * * @since 2.3.0 * * @param string $buffer The HTML buffer. * @param string $href The stylesheet href to locate. * @param int $offset Byte offset to search from. * @return array|null Array of [start, end, tag text], or null when no `<link>` tag carries the href. */functionfind_link_tag_for_href(,,){=(int);=0;while(<50){++;=strpos(,,);if(false===){returnnull;}=(>2048)?-2048:0;=substr(,,-);=(\'\'!==)?strrpos(,\'<\'):false;=(false!==)?+:false;=strpos(,\'>\',);if(false===||false===||<=){returnnull;}=substr(,,-+1);if(false!==stripos(,\'<link\')){returnarray(,,);}=+strlen();}returnnull;}/** * Whether an href is carried by a genuine `<link>` tag before `</head>`. * * Tag-verified head membership for {@see hoist_late_block_styles()}: * a bare href substring (script string, preload, comment) before the * head close does not count. Never uses regex. * * @since 2.3.0 * * @param string $buffer The HTML buffer. * @param string $href The stylesheet href to test. * @param int $head_close Byte offset of `</head>`. * @return bool True when a `<link>` tag carries the href inside head. */functionis_href_in_head_link(,,){=->find_link_tag_for_href(,,0);return(null!==&&[0]<);}/** * Process the buffer (image optimisation, minification, CDN rewrite) without saving. * * @param string $buffer The content to be processed. * @return string The processed buffer content. * * @since 2.0.0 */functionprocess_buffer_only(){if(!is_string()){return\'\';}if(\'\'===){return;}if(self::){return;}self::=true;=->hoist_late_block_styles();=->get_image_optimisation();=->maybe_serve_next_gen_images();=->add_delay_load_img();=->add_delay_load_backgrounds();=->lazy_load_videos();=->lazy_render_elements();if(!empty(->options[\'file_optimisation\'][\'hostGoogleFontsLocally\']??false)){=->get_google_fonts()->process_buffer();}if(!empty(->options[\'file_optimisation\'][\'fontMetricFallback\']??false)){=->get_google_fonts()->inject_metric_fallback();}=->options[\'file_optimisation\']??array();=!empty([\'minifyHTML\'])||!empty([\'delayJS\'])||!empty([\'minifyInlineCSS\'])||!empty([\'minifyInlineJS\']);if(){=->minify_buffer();}=->maybe_apply_used_css();=->maybe_apply_cdn();return;}/** * Filter callback for wp_template_enhancement_output_buffer (WP 6.9+). * * Processes the output buffer without saving to cache. Part of the WP 6.9 * template-enhancement buffer path adopted in {@see Main::setup_hooks()}: * wp_template_enhancement_output_buffer (filter at priority 10) + * wp_finalized_template_enhancement_output_buffer (action). Registering the * finalized action automatically opts into the buffer (priority 1000 by * default), which disables response streaming — TTFB increases while TTLB * unchanged; see {@see Main::emit_server_timing_header()} for the intentional * tradeoff (Server-Timing keeps disabled by default, emit only on cache-miss). * Returns filtered output via process_buffer_only(); persistence is handled * by {@see Cache::stash_cache()} / save_processed_buffer() to index.html+.gz+.br. * * @param string $filtered_output The filtered output from previous callbacks. * @param string $output The raw output buffer content. * @return string The processed output buffer. * * @since 2.0.0 */functionprocess_buffer_for_cache(,){if(!is_string()){if(is_string()&&\'\'!==){return;}return\'\';}try{if(!->is_cache_allowed_for_current_user()||->is_not_cacheable()){return;}->current_role_hash=->get_logged_in_role_hash();return->process_buffer_only();}catch(\\Throwable){do_action(\'wppo_debug_log\',\'WPPO page cache buffer processing failed.\',array(\'exception\'=>));if(is_string()&&\'\'!==){return;}if(is_string()&&\'\'!==){return;}return\'\';}}/** * Action callback for wp_finalized_template_enhancement_output_buffer (WP 6.9+). * * Stashes the processed output to the static cache files. Complements * {@see Cache::process_buffer_for_cache()} on wp_template_enhancement_output_buffer * (filter). Registering this finalized action opts into the template-enhancement * buffer (priority 1000 by default) which disables response streaming; TTFB * tradeoff is documented in {@see Main::emit_server_timing_header()} and * {@see Main::setup_hooks()} Server-Timing block. Persists via * save_processed_buffer() to index.html + .gz + .br. * * @param string $output The finalized output buffer content (alias $final). * @return void * * @since 2.0.0 */functionstash_cache(){if(!is_string()||\'\'===){return;}try{if(!->is_cache_allowed_for_current_user()||->is_not_cacheable()){return;}=!empty(->current_role_hash)?->current_role_hash:->get_logged_in_role_hash();=->get_cache_file_path(\'html\',);->save_processed_buffer(,);}catch(\\Throwable){do_action(\'wppo_debug_log\',\'WPPO page cache stash failed.\',array(\'exception\'=>));}}/** * Rewrite local asset URLs via CDN class (LS-410 parity). * * Scans img, script, link, source, and video tags and replaces attribute values that start with the site URL * and contain `/wp-content/` or `/wp-includes/`. The attributes handled are `src`, `href`, `data-src`, * `srcset`, and `data-srcset`. If no CDN is configured the buffer is returned unchanged. * * Delegates to CDN::rewrite_buffer() which handles tag attrs, srcset, inline url() and origin guards. * Constant LITESPEED_BYPASS_CDN short-circuits (LSCWP cdn.cls.php:106). * * @param string $buffer HTML buffer. * @return string The HTML with applicable asset URLs rewritten to the CDN, or the original HTML if no changes were made. * @since 1.2.0 * @since 2.0.0 Added LITESPEED_BYPASS_CDN guard and CDN delegation. */functionmaybe_apply_cdn(string):string{if(defined(\'LITESPEED_BYPASS_CDN\')&&LITESPEED_BYPASS_CDN){return;}if(class_exists(\'PerformanceOptimise\\Inc\\CDN\')){returnCDN::rewrite_buffer();}if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_Integration\')&&!LiteSpeed_Integration::can_apply_cdn()){return;}if(!apply_filters(\'wppo_litespeed_can_cdn\',true)){return;}if(has_filter(\'litespeed_can_cdn\')&&!apply_filters(\'litespeed_can_cdn\',true)){return;}return;}/** * Minify the output buffer. * * @param string $buffer The HTML content to be minified. * @return string The minified HTML content. * * @since 1.0.0 */functionminify_buffer(){if(->should_bypass_for_litespeed()){return;}=newMinify\\HTML(,->options);=->get_minified_html();return;}/** * Record the DONOTCACHEPAGE decision on disk and purge stale static files for the current URL. * * Writes a `.wppo-no-cache` marker file next to the cached HTML so the * advanced-cache.php drop-in (which boots before WordPress) can skip serving * a stale static copy. Runs at most once per request. Best-effort: this only * engages for pages that are actually rendered by WordPress at least once * after the constant is set — a page that is already cached and never re- * rendered stays stale until a cache clear, post invalidation, or the * one-time version-upgrade purge removes it. * * @return void * @since 1.9.0 */functionmaybe_mark_page_not_cacheable():void{if(->no_cache_marker_written){return;}->no_cache_marker_written=true;=->get_cache_file_path(\'html\');if(\'\'===){return;}=trailingslashit(dirname()).\'.wppo-no-cache\';if(!->is_path_contained()){->log_traversal_probe(->url_path);return;}=->get_filesystem();if(!){return;}if(->exists()){return;}if(!->prepare_cache_dir()){return;}=->put_contents(,(string)time(),FS_CHMOD_FILE);if(!){return;}->delete_cache_files();->delete_role_variant_files(dirname());}/** * Whether the current request is a WooCommerce AJAX endpoint. * * Matches the pretty-permalink /wc-ajax/... path segment (decoded, case-insensitive) and the * ?wc-ajax=... query parameter (via $_GET and the raw query string). * A bare substring (e.g. /my-wc-ajax-guide/) intentionally does NOT * match — only the exact path segment or parameter name bypasses * the cache (issue #907 review). * * Shared by is_not_cacheable() and maybe_store_cache() so the * storage layer refuses wc-ajax XHRs even if is_not_cacheable() is * bypassed via the wppo_should_cache_request filter. * * @since 2.0.0 * @return bool True for wc-ajax requests. */functionis_wc_ajax_request():bool{=wp_normalize_path(trim(rawurldecode((string)wp_parse_url(->request_uri,PHP_URL_PATH)),\'/\'));if(preg_match(\'#(^|/)wc-ajax(/|$)#i\',)){returntrue;}if(isset([\'wc-ajax\'])){returntrue;}return!empty([\'QUERY_STRING\'])&&(bool)preg_match(\'/(?:^|&)(wc-ajax)(?:=|&|$)/i\',sanitize_text_field(wp_unslash([\'QUERY_STRING\'])));}/** * Whether the current request targets a WooCommerce Store API route. * * Store API responses (`wc/store`, `wcstore`, `wp-json/wc/store*`, * `wp-json/wcstore*`) are dynamic JSON and must never be cached — * unconditional on the `wooSafeMode` toggle, mirroring wc-ajax. * Fail-open: detection failure returns true (treated as dynamic, never cached) and * the broader Woo guards still apply. * * @since 2.0.0 * @return bool True for Store API requests. */functionis_woo_store_api_request():bool{=wp_normalize_path(trim(rawurldecode((string)wp_parse_url(->request_uri,PHP_URL_PATH)),\'/\'));if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_store_api_request\')){try{returnWoo_Detect::is_woo_store_api_request();}catch(\\Throwable){unset();returntrue;}}if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_store_api_path\')){try{if(Woo_Detect::is_woo_store_api_path()){returntrue;}}catch(\\Throwable){unset();}try{=isset([\'rest_route\'])?sanitize_text_field(wp_unslash([\'rest_route\'])):\'\';if(\'\'!==&&Woo_Detect::is_woo_store_api_path()){returntrue;}}catch(\\Throwable){unset();}}if((bool)preg_match(\'#(^|/)(?:wc/store|wcstore|wp-json/wc/store|wp-json/wcstore)(/|$)#i\',\'/\'.)){returntrue;}=isset([\'rest_route\'])?sanitize_text_field(wp_unslash([\'rest_route\'])):\'\';if(\'\'!==&&(bool)preg_match(\'#(^|/)(?:wc/store|wcstore|wp-json/wc/store|wp-json/wcstore)(/|$)#i\',\'/\'.ltrim(,\'/\'))){returntrue;}return!empty([\'QUERY_STRING\'])&&(bool)preg_match(\'#rest_route=[^&]*(?:wc/store|wcstore)#i\',rawurldecode(sanitize_text_field(wp_unslash([\'QUERY_STRING\']))));}/** * Whether the current request should be excluded from static cache due to WooCommerce safe mode. * * Safe-by-default exclusions for WooCommerce: cart/checkout/account, * wc-ajax, add-to-cart, and Woo session/cart cookies. Fail-open: any * detection failure treats the page as non-cacheable (never fatal). When * Woo symbols are missing the conditional-function branch is skipped but * URI/cookie guards remain so hardcoded cart/checkout slugs stay safe even * on non-Woo installs (legacy behaviour preserved, 0 queries). * * @since 2.0.0 * @return bool True when the request is Woo-excluded (not cacheable). */functionis_woo_excluded():bool{try{if(->is_woo_store_api_request()){returntrue;}}catch(\\Throwable){unset();}if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_safe_mode_enabled\')){try{if(!Woo_Detect::is_woo_safe_mode_enabled(is_array(->options)?->options:null)){returnfalse;}}catch(\\Throwable){unset();returntrue;}}elseif(isset(->options[\'cache_settings\'][\'wooSafeMode\'])&&false===->options[\'cache_settings\'][\'wooSafeMode\']){returnfalse;}try{=false;if(function_exists(\'is_wc_endpoint_url\')){try{if(is_wc_endpoint_url()){=true;}}catch(\\Throwable){unset();=true;}}=function_exists(\'is_cart\')||function_exists(\'is_checkout\')||function_exists(\'is_account_page\')||function_exists(\'is_woocommerce\')||class_exists(\'WooCommerce\',false);if(!&&){if(function_exists(\'is_cart\')&&is_cart()){=true;}elseif(function_exists(\'is_checkout\')&&is_checkout()){=true;}elseif(function_exists(\'is_account_page\')&&is_account_page()){=true;}}if(!){=wp_parse_url(->request_uri,PHP_URL_PATH);=wp_normalize_path(trim(rawurldecode((string)),\'/\'));=false;if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_dynamic_path\')){try{=Woo_Detect::is_woo_dynamic_path();}catch(\\Throwable){unset();=true;}}else{=(bool)preg_match(\'#^/(?:cart|checkout|my-account)(?:/|$)#i\',\'/\'.);}if(){=true;}}if(!){if(!empty([\'woocommerce_items_in_cart\'])||!empty([\'woocommerce_cart_hash\'])){=true;}}if(!&&!empty()&&is_array()){=function_exists(\'wp_unslash\')?wp_unslash():;foreach(as=>){=(string);if(function_exists(\'sanitize_key\')){=sanitize_key();}if(0===strpos(,\'wp_woocommerce_session_\')&&!empty()){=true;break;}}}if(!){if(->is_wc_ajax_request()){=true;}}if(!){if(isset([\'add-to-cart\'])){=true;}elseif(!empty([\'QUERY_STRING\'])&&preg_match(\'/(?:^|&)(add-to-cart)(?:=|&|$)/i\',sanitize_text_field(wp_unslash([\'QUERY_STRING\'])))){=true;}}if(!){try{if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_faceted_query\')){=(string)wp_parse_url(->request_uri,PHP_URL_QUERY);if(\'\'===&&!empty([\'QUERY_STRING\'])){=sanitize_text_field(wp_unslash([\'QUERY_STRING\']));}if(\'\'!==&&Woo_Detect::is_woo_faceted_query()){=true;}}}catch(\\Throwable){unset();=true;}}if(!){returnfalse;}if(function_exists(\'has_filter\')&&has_filter(\'wppo_woo_cacheable\')){=(bool)apply_filters(\'wppo_woo_cacheable\',false,->request_uri);if(){returnfalse;}}returntrue;}catch(\\Throwable){unset();returntrue;}}/** * Whether the current response carries a Set-Cookie header. * * Commerce-safety store refusal (issue #1307): a response carrying * Set-Cookie (Woo session, auth, consent) is per-visitor dynamic and * must never be persisted to the static file cache. The pre-boot * drop-in cannot inspect response headers at serve time, so this * write-path check is the enforcement point. Cheap: one * function_exists plus one headers_list scan, only on commerce * relevant write attempts after the early-out guards. Fail-open: * any detection failure returns true (refuse the store, dynamic), * never fatal. * * @since 2.2.0 * @return bool True when a Set-Cookie response header is present. */functionhas_set_cookie_response_header():bool{try{if(!function_exists(\'headers_list\')){returnfalse;}foreach(headers_list()as){if(0===stripos((string),\'set-cookie:\')){returntrue;}}returnfalse;}catch(\\Throwable){unset();returntrue;}}/** * Whether the current request is an admin/editor-preview context. * * Unconditional static-cache bypass (issue #1097): wp-admin / login / * admin-ajax paths, `is_admin()`, `is_preview()`, * `is_customize_preview()`, Elementor preview mode, AJAX/REST/JSON, * and builder/core preview query params via * `Util::is_editor_preview_request()`. No re-allow filter by design: * editor output must never be written to or served from the static * cache. Fail-open: any detection failure bypasses the cache. * * @since 2.2.0 * @return bool True when the request must bypass the cache. */functionis_editor_preview_excluded():bool{if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'is_editor_preview_request\')){try{returnUtil::is_editor_preview_request();}catch(\\Throwable){unset();returntrue;}}try{if(function_exists(\'is_admin\')){try{if(is_admin()){returntrue;}}catch(\\Throwable){unset();returntrue;}}if(function_exists(\'is_preview\')){try{if(is_preview()){returntrue;}}catch(\\Throwable){unset();returntrue;}}if(function_exists(\'is_customize_preview\')){try{if(is_customize_preview()){returntrue;}}catch(\\Throwable){unset();returntrue;}}=wp_normalize_path(trim(rawurldecode((string)wp_parse_url(->request_uri,PHP_URL_PATH)),\'/\'));if((bool)preg_match(\'#(^|/)(?:wp-admin|wp-login\\.php|admin-ajax\\.php)(/|$)#i\',\'/\'.)){returntrue;}=class_exists(\'PerformanceOptimise\\Inc\\Util\')?Util::EDITOR_PREVIEW_PARAMS:array(\'elementor-preview\',\'et_fb\',\'et_pb_preview\',\'vc_action\',\'vc_editable\',\'bricks\',\'preview\',\'preview_id\',\'customize_changeset_uuid\',\'customizer\');foreach(as){if(isset([])){returntrue;}}returnfalse;}catch(\\Throwable){unset();returntrue;}}/** * Check if the page is not cacheable. * * Note: for pages opted out via the DONOTCACHEPAGE constant this also records * the decision on disk (see maybe_mark_page_not_cacheable()). This coupling is * intentional: every render of an opted-out page runs through this predicate * before any buffer/storage path can react, so it is the only reliable place * to write the marker the drop-in checks. Such pages also skip output-buffer * optimisations, matching how every other non-cacheable page behaves. * * @return bool * * @since 1.0.0 */functionis_not_cacheable():bool{if(\'\'===->cache_root_dir){returntrue;}if(empty(->domain)){returntrue;}if(->host_mismatch){returntrue;}if(defined(\'DONOTCACHEPAGE\')&&DONOTCACHEPAGE){->maybe_mark_page_not_cacheable();returntrue;}try{if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'has_uncacheable_query\')&&Util::has_uncacheable_query()){returntrue;}}catch(\\Throwable){unset();returntrue;}/** * Filters whether the current request should be cached. * * Placed after the DONOTCACHEPAGE constant check so the constant * always wins even if the filter returns true. Return false to skip * ob_start and cache storage. * * @since 2.0.0 * * @param bool $should_cache Whether the request should be cached. Default true. * @param string $request_uri The request URI. * @param bool $is_mobile Whether the request is from a mobile device. * @param bool $is_logged_in Whether the user is logged in. */=function_exists(\'wp_is_mobile\')?wp_is_mobile():false;=function_exists(\'is_user_logged_in\')?is_user_logged_in():false;=(bool)apply_filters(\'wppo_should_cache_request\',true,->request_uri,,);if(!){returntrue;}=wp_parse_url(->request_uri,PHP_URL_PATH);=wp_normalize_path(trim(rawurldecode((string)),\'/\'));if(false!==strpos(,\"\\0\")||false!==strpos(,\'..\')){returntrue;}if(function_exists(\'is_feed\')&&is_feed()){returntrue;}if(preg_match(\'/(?:sitemap[^\\/]*\\.xml|wp-sitemap[^\\/]*\\.xml|\\.xml)$/i\',)){returntrue;}try{if(->is_editor_preview_excluded()){returntrue;}}catch(\\Throwable){unset();returntrue;}if(->is_woo_store_api_request()){returntrue;}if(->is_woo_excluded()){returntrue;}if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_ESI\',false)){=false;try{if(LiteSpeed_ESI::should_punch_hole(\'cart\')||LiteSpeed_ESI::should_punch_hole(\'checkout\')||LiteSpeed_ESI::should_punch_hole(\'account\')||LiteSpeed_ESI::should_punch_hole(\'adminbar\')){=true;}}catch(\\Throwable){=false;}if(){if(!defined(\'DONOTCACHEPAGE\')){define(\'DONOTCACHEPAGE\',true);}->maybe_mark_page_not_cacheable();returntrue;}}=pathinfo(,PATHINFO_EXTENSION);returnis_404()||!empty();}/** * Get the cache file path based on the URL path. * * @param string $type The file type (default: \'html\'). * @param string $role_hash Optional role hash for logged-in user cache variant. * @param string $variant Optional variant suffix (e.g. combined-CSS state) baked into the file name. * @return string The cache file path. * * @since 1.0.0 */functionget_cache_file_path(=\'html\',string=\'\',string=\'\'):string{if(\'\'!==&&!preg_match(\'/^[a-z0-9-]{1,32}$/i\',)){->log_traversal_probe();return\'\';}if(\'\'!==&&!preg_match(\'/^[a-z0-9-]{1,32}$/i\',)){->log_traversal_probe();return\'\';}=?\"-{}\":\'\';if(){.=\"-{}\";}if(!preg_match(\'/^[a-z0-9]+$/i\',(string))){->log_traversal_probe((string));return\'\';}=\"index{}.{}\";return->safe_path_for_url(->url_path,);}/** * Get the cache file URL based on the URL path. * * @param string $type The file type (default: \'html\'). * @param string $variant Optional variant suffix baked into the file name. * @return string The cache file URL. * * @since 1.0.0 */functionget_cache_file_url(=\'html\',string=\'\'):string{if(\'\'===->cache_root_url){return\'\';}if(\'\'!==&&!preg_match(\'/^[a-z0-9-]{1,32}$/i\',)){->log_traversal_probe();return\'\';}if(!preg_match(\'/^[a-z0-9]+$/i\',(string))){->log_traversal_probe((string));return\'\';}=?\"-{}\":\'\';=\"index{}.{}\";=->safe_path_for_url(->url_path,);if(\'\'===){return\'\';}=(\'\'===->url_path?:\"{->url_path}/{}\");return\"{->cache_root_url}/{->domain}/{}\";}/** * Apply used-CSS to the buffer if the setting is enabled. * * @param string $buffer The HTML buffer. * @return string The processed buffer. * * @since 1.9.0 */functionmaybe_apply_used_css(string):string{=newUsed_CSS(->options);return->process_buffer();}/** * Prepare the cache directory for storing files. * * @return bool True if successful, false otherwise. * * @since 1.0.0 */functionprepare_cache_dir():bool{=->safe_path_for_url(->url_path,\'index.html\');if(\'\'===){returnfalse;}if(function_exists(\'dirname\')){=dirname();}else{=\"{->cache_root_dir}/{->domain}/\".(\'\'===->url_path?\'\':\"/{->url_path}\");}if(!->is_path_contained(trailingslashit())){->log_traversal_probe(->url_path);returnfalse;}if(!Util::prepare_cache_dir()){returnfalse;}if(!->is_path_contained(trailingslashit())){->log_traversal_probe(->url_path);returnfalse;}returntrue;}/** * Atomically write contents to a file via tmp+rename. * * Writes to a temporary sibling file in the same directory and then * atomically moves it to the final path, preventing readers from * observing a partially-written cache file during stampede writes. * The final path must originate from {@see safe_path_for_url()}; the * containment pre-check below is defense-in-depth on the resolved path. * * @since 2.0.0 * @param string $path Final file path. * @param string $contents File contents. * @return bool True on success. */functionatomic_put_contents(string,string):bool{if(\'\'===||!->is_path_contained()){->log_traversal_probe();returnfalse;}=->get_filesystem();if(!){returnfalse;}returnUtil::atomic_file_put_contents(,,);}/** * Save cache files with optional gzip compression. * * The file path must originate from {@see safe_path_for_url()} (via * `get_cache_file_path()`); the containment guard below plus the * `atomic_put_contents()` re-check on the base and `.gz`/`.br` * siblings are defense-in-depth on resolved paths. * * @param string $buffer The content to save. * @param string $file_path The file path for saving. * @param string $type The file type (default: \'html\'). * @return void * * @since 2.0.0 */functionsave_cache_files(,,=\'html\'):void{if(\'\'===(string)||!->is_path_contained((string))){->log_traversal_probe((string));return;}if(\'html\'===&&!->maybe_store_cache()){return;}if(\'html\'===){=Util::cached_home_url(->request_uri);=apply_filters(\'wppo_cache_page_html\',,);}=.\'.gz\';=.\'.br\';=->get_filesystem();if(!){return;}=Util::transient_key(\'wppo_cache_write_\'.md5());=Util::generate_stampede_owner();=Util::stampede_lock_ttl();if(!Util::acquire_stampede_lock(,,)){return;}try{->atomic_put_contents(,);if(function_exists(\'gzencode\')){=gzencode(,9);if(false!==){->atomic_put_contents(,);}}if(\'html\'===){=false;if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_Integration\')&&method_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_Integration\',\'is_brotli_enabled\')){=LiteSpeed_Integration::is_brotli_enabled();}else{=Util::get_settings();=!empty([\'litespeed_integration\'][\'enableBrotli\']);=extension_loaded(\'brotli\')||function_exists(\'brotli_compress\');=&&;/** * Filter whether brotli generation is enabled (fallback). * * @since 2.0.0 * @param bool $use_brotli Whether brotli is enabled. */=(bool)apply_filters(\'wppo_litespeed_brotli\',);}if(&&function_exists(\'brotli_compress\')){try{=brotli_compress(,4,0);if(false!==&&is_string()){->atomic_put_contents(,);}}catch(\\Throwable){unset();}}}if(\'html\'===){->delete_cache_files(trailingslashit(dirname()).\'.wppo-no-cache\');}}finally{Util::release_stampede_lock(,);}if(\'html\'===){try{self::maybe_enforce_cache_cap();}catch(\\Throwable){unset();}}}/** * Save processed buffer with filesystem guard (shared by legacy and WP 6.9+ paths). * * The file path must originate from {@see safe_path_for_url()}; the * containment guard below is defense-in-depth on the resolved path. * * @param string $buffer The processed buffer content. * @param string $file_path The file path for saving. * @return void * * @since 2.0.0 */functionsave_processed_buffer(string,string):void{if(\'\'===||!->is_path_contained()){->log_traversal_probe();return;}if(!->get_filesystem()||!->prepare_cache_dir()){return;}->save_cache_files(,);}/** * Determine if cache storage is allowed. * * @return bool True if cache can be stored, false otherwise. * * @since 1.0.0 */functionmaybe_store_cache(){if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_ESI\',false)){try{if(LiteSpeed_ESI::should_punch_hole(\'cart\')||LiteSpeed_ESI::should_punch_hole(\'checkout\')||LiteSpeed_ESI::should_punch_hole(\'account\')||LiteSpeed_ESI::should_punch_hole(\'adminbar\')||LiteSpeed_ESI::should_punch_hole(\'nonce\')){if(defined(\'DONOTCACHEPAGE\')&&DONOTCACHEPAGE){->maybe_mark_page_not_cacheable();}elseif(!defined(\'DONOTCACHEPAGE\')){define(\'DONOTCACHEPAGE\',true);->maybe_mark_page_not_cacheable();}returnfalse;}}catch(\\Throwable){unset();}}if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_Integration\')&&LiteSpeed_Integration::is_litespeed()&&!LiteSpeed_Integration::is_wppo_cache_owner()){if(->is_not_cacheable()){if(has_action(\'litespeed_control_set_nocache\')){do_action(\'litespeed_control_set_nocache\',\'wppo not cacheable (ls owns)\');}elseif(!headers_sent()){header(\'X-LiteSpeed-Cache-Control: no-cache\');}}/** * Filter whether WPPO file cache storage should be bypassed on LiteSpeed. * * @since 2.0.0 * @param bool $bypass Whether to bypass file cache. */=(bool)apply_filters(\'wppo_litespeed_bypass_file_cache\',true);if(){returnfalse;}}if(defined(\'DONOTCACHEPAGE\')&&DONOTCACHEPAGE){->maybe_mark_page_not_cacheable();returnfalse;}if(empty(->domain)||->host_mismatch||->path_rejected||false!==strpos(->url_path,\"\\0\")||false!==strpos(->url_path,\'..\')){returnfalse;}try{if(->is_editor_preview_excluded()){returnfalse;}}catch(\\Throwable){unset();returnfalse;}try{if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'has_uncacheable_query\')){if(Util::has_uncacheable_query()){returnfalse;}=isset([\'QUERY_STRING\'])?(string)[\'QUERY_STRING\']:\'\';if(function_exists(\'wp_unslash\')){=wp_unslash();}if(function_exists(\'sanitize_text_field\')){=sanitize_text_field();}if(\'\'!==trim()){returnfalse;}}}catch(\\Throwable){unset();returnfalse;}if(->is_wc_ajax_request()){returnfalse;}if(->is_woo_store_api_request()){returnfalse;}try{if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_faceted_query\')){=(string)wp_parse_url(->request_uri,PHP_URL_QUERY);if(\'\'===&&!empty([\'QUERY_STRING\'])&&function_exists(\'wp_unslash\')&&function_exists(\'sanitize_text_field\')){=sanitize_text_field(wp_unslash([\'QUERY_STRING\']));}if(\'\'!==&&Woo_Detect::is_woo_faceted_query()){returnfalse;}}}catch(\\Throwable){unset();returnfalse;}try{if(->has_set_cookie_response_header()){returnfalse;}}catch(\\Throwable){unset();returnfalse;}if(->is_woo_excluded()){returnfalse;}if(!empty(->options[\'preload_settings\'][\'enablePreloadCache\'])){if(!empty(->options[\'preload_settings\'][\'excludePreloadCache\'])){=Util::process_urls(->options[\'preload_settings\'][\'excludePreloadCache\']);=->request_uri;=wp_parse_url(Util::cached_home_url(),PHP_URL_PATH)??\'\';if(&&\'/\'!==&&0===strpos(,)){=substr(,strlen());}=Util::cached_home_url();if(Util::is_url_excluded(,)){returnfalse;}}}returntrue;}/** * Whether current page request is cacheable (public wrapper for LiteSpeed). * * Renamed from is_request_cacheable() (issue #905) to avoid confusion * with LiteSpeed_Integration::is_request_cacheable(), which has * different semantics (adds query-string + preload-exclusion gates on * top of this predicate). Mirrors is_not_cacheable() for external * callers (e.g. LiteSpeed header emission) without exposing private * internals. Cheap — creates no I/O beyond what is_not_cacheable() * already does. * * @since 2.0.0 * @return bool True if cacheable. */functionis_page_cacheable():bool{return!->is_not_cacheable();}/** * Invalidate dynamic static HTML cache for a specific page and global archives. * * @param int $page_id The page ID. * @return void * * @since 1.0.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::invalidate_dynamic_static_html}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functioninvalidate_dynamic_static_html():void{->invalidator()->invalidate_dynamic_static_html();}/** * Invalidate the static HTML cache for a single post URL only. * * Used by the per-page delay kill-switch (#1037) so toggling * `_wppo_delay_disabled` takes effect on that URL without a full purge * and without the home/archive fan-out of * {@see invalidate_dynamic_static_html()}: only the post permalink\'s * `index.html` (+ gzip/brotli variants, role variants, no-cache marker, * and css/used-css sidecars) is deleted. Multisite-safe: per-site * `get_permalink()` plus domain-based `get_file_path()`, so no * cross-site leakage. Fail-open: any failure is swallowed — callers must * never fatal a meta save. No new WP/PHP APIs; safe on WP 6.2+ / PHP 8.2+. * * Bulk callers (e.g. Elementor bulk regen, issue #1259) pass * $bump_stats=false and rely on the deferred full purge — which bumps * once — instead of paying 6x delete_transient + get_option + * update_option per post inline. * * @since 2.0.0 * @param int $page_id Post ID whose single URL cache must be purged. * @param bool $bump_stats Whether to bump dashboard stats (6x transient * deletes + option write). Default true. * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::invalidate_single_static_html}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functioninvalidate_single_static_html(int,bool=true):void{->invalidator()->invalidate_single_static_html(,);}/** * Surgically invalidate cache for a WooCommerce product, order, or coupon. * * Purges only the object\'s own permalink path (+ css/used-css sidecars) * plus, for products, its product-category/tag archive paths and the * shop page path. Never purges the home page, never calls * clear_cache() (no full-cache wipe), and never schedules preload for * Woo-excluded permalinks. Multisite-safe: per-site get_permalink() + * domain-based get_file_path(), no cross-site purge. * * @since 2.0.0 * @param int $object_id Woo object (product/order/coupon) ID. * @param string $kind Object kind: \'product\', \'order\', or \'coupon\'. * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::invalidate_woo_object}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functioninvalidate_woo_object(int,string):void{->invalidator()->invalidate_woo_object(,);}/** * Whether a traversal probe has been logged this request. * * Rate-limits activity-log writes so a hostile crawler cannot flood * the log table with one entry per request path probe. * * @since 2.0.0 * @var bool */staticbool=false;/** * Sanitize a URL path for cache file mapping. * * Uses only the PHP_URL_PATH component, applies exactly one * rawurldecode pass (single-decode semantics: `%252e` stays encoded * on disk and is never re-decoded), then rejects null bytes, any * remaining `..` segments, and Windows drive prefixes. Returns an * empty string for hostile or empty input. * * Delegates to the shared {@see Util::sanitize_cache_url_path()} * helper so every file-writing surface normalizes identically. * * @since 2.0.0 * @since 2.2.0 Added the optional $allowed_host foreign-host refusal. * @param string|null $url_path Raw URL path or URL. * @param string|null $allowed_host Optional canonical host; threaded to the shared helper so * absolute-form callers refuse foreign hosts. * @return string Sanitized relative path or empty string. */staticfunctionsanitize_cache_url_path(?string,?string=null):string{returnUtil::sanitize_cache_url_path(,);}/** * Whether an absolute path stays inside the cache tree. * * Dual-prefix containment: the normalized path must start with both * the cache root and the per-domain directory (trailing-slash aware * so `wppo-evil` never prefix-matches `wppo`). Empty root or domain * fails closed. Since NEXT the check is symlink-aware: the resolved * target must also pass {@see Util::is_realpath_contained()} so a * symlink planted inside the cache tree cannot redirect a write * outside the root (CVE-2026-18051 class). On containment failure * callers skip the write and serve dynamically uncached. * * @since 2.0.0 * @since 2.2.0 Added realpath symlink containment. * @param string $path Absolute file or directory path. * @return bool True when contained. */functionis_path_contained(string):bool{try{returnUtil::validate_cache_write_path(->cache_root_dir,->domain,);}catch(\\Throwable){unset();returnfalse;}}/** * Single choke-point mapping a URL path + leaf filename to a contained absolute path. * * The ONLY function that maps a URL path + leaf filename to an absolute * filesystem path under `wp-content/cache/wppo/{domain}/{path}/`. Every * cache write and purge funnels through this gate: `get_cache_file_path()`, * `get_cache_file_url()`, `get_file_path()`, and `prepare_cache_dir()` * delegate here, while `atomic_put_contents()`, `save_cache_files()`, * `save_processed_buffer()`, `delete_cache_files()`, * `delete_no_cache_marker()`, `delete_role_variant_files()`, and the * `clear_cache()` single-page branch re-check containment via * {@see is_path_contained()} as defense-in-depth on already-resolved * paths. Returns an empty string on any rejection; callers fail open * (serve dynamic/uncached) and never touch the filesystem. `.htaccess` * writers are never called from this path. * * Internal steps (single place): fail-closed on empty root/domain or * a construction-time `path_rejected` flag; leaf-filename allowlist * (`index` plus optional `-[a-z0-9-]{1,32}` suffix groups with an * `[a-z0-9]+` extension, optional `.gz`/`.br` sibling suffix, plus the * explicit `used-css.css` alternative; length * <= 64; no `/`, `\\`, NUL, or `..`); domain allowlist via * `Util::normalize_cache_host()` (fail-closed on `\'\'`); resolution via * `Util::sanitize_cache_path()` (single-decode + NUL/dot-dot/drive/UNC * rejection + dual-prefix containment); final `is_path_contained()` * re-check; `log_traversal_probe()` on reject (skipped for the benign * homepage so `\'\'`/`\'/\'` never logs). * * @since 2.0.0 * @param string $url_path_or_url Raw URL path or URL. * @param string $filename Leaf filename (e.g. `index.html`). * @return string Contained absolute path, or \'\' when refused. */functionsafe_path_for_url(string,string):string{=(string);if(\'\'===->cache_root_dir||\'\'===->domain){return\'\';}if(->path_rejected){return\'\';}if(\'\'===->domain||false!==strpos(->domain,\'/\')||false!==strpos(->domain,\'\\\\\')||false!==strpos(->domain,\'..\')){return\'\';}=(string);if(\'\'===||strlen()>64){->log_traversal_probe();return\'\';}if(false!==strpos(,\'/\')||false!==strpos(,\'\\\\\')||false!==strpos(,\"\\0\")||false!==strpos(,\'..\')){->log_traversal_probe();return\'\';}=false;if(function_exists(\'preg_match\')){=(bool)preg_match(\'/^(?:index(?:-[a-z0-9-]{1,32})*\\.[a-z0-9]+(?:\\.(?:gz|br))?|used-css\\.css(?:\\.(?:gz|br))?)$/i\',);}else{=(\'index.html\'===||\'used-css.css\'===);}if(!){->log_traversal_probe();return\'\';}=\'\';if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'sanitize_cache_path\')){try{=Util::sanitize_cache_path(->cache_root_dir,->domain,,);}catch(\\Throwable){unset();=\'\';}}if(\'\'===){->log_homepage_aware_probe();return\'\';}if(!->is_path_contained()){->log_traversal_probe();return\'\';}return;}/** * Log a rejected path unless it is the benign homepage. * * `Util::sanitize_cache_path()` returns `\'\'` for both the benign * homepage (`\'\'`/`\'/\'`) and hostile inputs; only the latter is a probe. * * @since 2.0.0 * @param string $raw_input The raw input that resolved to \'\'. * @return void */functionlog_homepage_aware_probe(string):void{=null;try{if(function_exists(\'wp_parse_url\')){=wp_parse_url(,PHP_URL_PATH);}elseif(function_exists(\'parse_url\')){=parse_url(,PHP_URL_PATH);}else{=;}}catch(\\Throwable){unset();=;}if(null===||false===){=;}if(\'\'!==trim(trim((string)),\'/\')){->log_traversal_probe();}}/** * Best-effort cross-request throttle check. * * Returns true only when the transient transport positively reports a * stored value. A missing transport, a miss (false/null), or a * throwing transport all count as \"not throttled\" so a broken * throttle can never suppress the log it guards (fail-open toward * logging). * * @since 2.2.0 * @param string $throttle_key Throttle transient key. * @param int $ttl TTL in seconds when recording a fresh hit. * @return bool True when a previous hit is still recorded. */functionis_throttled(string,int):bool{try{if(!function_exists(\'get_transient\')||!function_exists(\'set_transient\')){returnfalse;}=get_transient();if(false!==&&null!==){returntrue;}try{set_transient(,1,);}catch(\\Throwable){unset();}}catch(\\Throwable){unset();}returnfalse;}/** * Log a blocked cache path traversal probe (once per request + throttled across requests). * * The once-per-request static gate alone lets an unauthenticated * crawler insert one wppo_activity_logs row per request (log-table * bloat / DB DoS), so a short-TTL transient gate per probe hash * throttles cross-request repeats (mirroring the * log_inline_budget_drift throttle). Never throws: failures degrade * silently to serving uncached. * * @since 2.0.0 * @param string $raw_input The hostile input that was rejected. * @return void */functionlog_traversal_probe(string):void{if(self::){return;}self::=true;try{if(!class_exists(\'PerformanceOptimise\\Inc\\Log\')){return;}if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'transient_key\')){=Util::transient_key(\'wppo_probe_\'.md5(substr((string),0,64)));=defined(\'HOUR_IN_SECONDS\')?HOUR_IN_SECONDS:3600;if(->is_throttled(,)){return;}}=str_replace(\"\\0\",\'\',(string));if(function_exists(\'sanitize_text_field\')){=sanitize_text_field();}=substr(,0,200);=function_exists(\'__\')?__(\'Blocked cache path traversal probe.\',\'performance-optimisation\'):\'Blocked cache path traversal probe.\';if(\'\'!==){.=\' \'.;}Log::add();}catch(\\Throwable){unset();}}/** * Get the file path for a specific page. * * Hardened against cache-key path traversal (CVE-2026-3129 follow-up): * only the PHP_URL_PATH component is used, exactly one rawurldecode * pass is applied, and null bytes plus `..` segments are rejected. * The resolved path must pass dual-prefix containment before it is * returned; otherwise an empty string is returned and the probe is * logged so the request is served uncached. * * @param string|null $url_path The URL path (optional). * @param string $type The file type (default: \'html\'). * @return string The file path. * * @since 1.1.1 */functionget_file_path(?string=null,string=\'html\'):string{=(string);if(\'used-css\'===){=\'used-css.css\';}else{if(!preg_match(\'/^[a-z0-9]+$/i\',(string))){->log_traversal_probe();return\'\';}=\"index.{}\";}return->safe_path_for_url(,);}/** * Map a derived asset path to its sibling last-good fallback path. * * Thin wrapper over {@see Util::get_purge_fallback_path_for()} so the * purge/miss path can be unit-tested via the Cache surface. Returns * `\'\'` for non CSS/JS paths and for paths that already point at a * fallback file (loop guard). * * @param string $file_path Absolute derived-asset path. * @return string Sibling fallback path, or \'\' when not applicable. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::get_purge_fallback_path}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionget_purge_fallback_path(string):string{returnCache_Invalidator::get_purge_fallback_path();}/** * Retain a last-good fallback copy before a derived file is purged. * * Thin wrapper over {@see Util::retain_purge_fallback_file()} (the * single shared implementation — see also * `Used_CSS::retain_purge_fallback()`). No-op when disabled, for * non CSS/JS paths, on containment failure, or when the base file * is missing/empty. Never throws. * * @param string $file_path The derived file about to be deleted. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::retain_purge_fallback}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionretain_purge_fallback(string):void{->invalidator()->retain_purge_fallback();}/** * Resolve a post-purge miss under the cache path to its fallback. * * Returns `served=true` with a redirect status (default 302 via * {@see purge_fallback_redirect_status()}) and the fallback * path/URL only when every guard holds: the gate is on, the requested path is * contained, it is not itself a fallback file (no loops), the base * file is missing, and a non-empty fallback sibling exists. A single * throttled log entry is written per fallback directory per day so a * sustained miss storm cannot flood the activity log. All other cases * return `served=false` with status 404 (legacy hard-404 preserved, * including when the gate is off). Never throws. * * @param string $requested_path Absolute requested derived-asset path. * @return array{served: bool, status: int, fallback_path: string, location: string} Resolution. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::get_purge_fallback_response}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionget_purge_fallback_response(string):array{return->invalidator()->get_purge_fallback_response();}/** * Resolve the redirect status for a fallback serve. * * Filterable via `wppo_purge_fallback_redirect_status` (default * {@see PURGE_FALLBACK_REDIRECT_STATUS}); invalid values fall back * to the constant. Never throws. * * @since 2.2.0 * @return int Redirect status code. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::purge_fallback_redirect_status}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionpurge_fallback_redirect_status():int{returnCache_Invalidator::purge_fallback_redirect_status();}/** * Build the headers for a fallback redirect (pure, testable). * * The `Cache-Control: no-store` line is deliberate: without it * browsers/proxies may heuristically cache the 302 and keep * redirecting to `fallback.css` after the base regenerates — a * stale-serve window regeneration cannot clear client-side. * * @since 2.2.0 * @param string $location Absolute fallback URL. * @param int $status Redirect status code. * @return array{location: string, status: int, headers: string[]} Headers to send. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::build_purge_fallback_headers}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionbuild_purge_fallback_headers(string,int):array{returnCache_Invalidator::build_purge_fallback_headers(,);}/** * Serve a resolved purge fallback with a redirect. * * Thin sender for {@see get_purge_fallback_response()}: no-op unless * the resolution carries `served=true` with a non-empty location. * Uses `wp_safe_redirect()` when available, `header()` otherwise, * and sends `Cache-Control: no-store` so the redirect itself is * never cached past regeneration. The location is stripped of * literal and encoded CR/LF before sending (defense-in-depth: it is * internally built, but this method is public). Never throws; * terminates the request on success via `exit` (skipped when the * `WPPO_PURGE_FALLBACK_NO_EXIT` test seam is set, returning true). * * @param array{served: bool, status: int, fallback_path: string, location: string} $response Resolver output. * @return bool True when the fallback was served (or would be, under the test seam). * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::serve_purge_fallback_response}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionserve_purge_fallback_response(array):bool{return->invalidator()->serve_purge_fallback_response();}/** * Serve the last-good fallback for Nginx `?wppo_purge_fallback=` misses. * * Paired with the Nginx snippet emitted by * {@see Server_Rules::get_nginx_rules()}: `try_files` falls through to * `index.php?wppo_purge_fallback=$uri` on a miss under the cache path, * and this handler 302s to the sibling fallback when one was retained. * No-op when the gate is off, when the query var is absent, or when no * fallback resolves (legacy 404 flow continues). Never throws. * * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::maybe_serve_purge_fallback}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionmaybe_serve_purge_fallback():void{->invalidator()->maybe_serve_purge_fallback();}/** * Log a purge-fallback serve (global once-per-day throttle). * * Shares the single `wppo_purge_fallback_served` transient via * {@see Util::purge_fallback_should_log()} so a sustained post-purge * miss storm writes one activity-log row per day total (not one per * directory). Fail-open: logging failures never affect serving. * * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::log_purge_fallback}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionlog_purge_fallback():void{->invalidator()->log_purge_fallback();}/** * Delete used-CSS file for a specific file path. * * @param string $file_path The used-css file path. * @return bool True if successful (or not exists), false otherwise. * * @since 1.9.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_used_css_file}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_used_css_file(string):bool{return->invalidator()->delete_used_css_file();}/** * Delete the DONOTCACHEPAGE marker that lives beside a cached HTML file. * * @param string $html_file_path The HTML cache file path whose directory holds the marker. * @return void * * @since 1.9.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_no_cache_marker}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_no_cache_marker(string):void{->invalidator()->delete_no_cache_marker();}/** * Delete cache files for a specific file path. * * The file path must originate from {@see safe_path_for_url()}; the * base plus `.gz`/`.br` sibling containment checks below are * defense-in-depth on resolved paths. * * @param string $file_path The file path. * @return bool True if successful (or not exists), false otherwise. * * @since 1.1.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_cache_files}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_cache_files():bool{return->invalidator()->delete_cache_files();}/** * Delete all index-{hash}.html role-variant cache files in a directory. * * The directory must derive from a {@see safe_path_for_url()}-resolved * path (e.g. `dirname()` of a choke-point result); the directory and * per-file containment checks below are defense-in-depth. * * @param string $dir Directory to scan. * @return void * @since 1.9.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_role_variant_files}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_role_variant_files(string):void{->invalidator()->delete_role_variant_files();}/** * Clear the cache for a specific page or all pages. * * Also flushes any static HTML pages that speculative prerendering * (speculation rules) may have requested and cached: such requests are * ordinary GETs that produce the same per-URL static files (plus their * `.gz` variants and role variants) served to every other visitor, so * the full clear below removes the whole domain directory and the * single-page clear removes the page\'s HTML, gzip, and role-variant * copies. A stale prerendered copy is therefore never served after * invalidation. * * @param string|null $url_path The URL path of the page for which to clear the cache. If null, all cache will be cleared. * @return bool True on success, false on failure. * * @since 1.1.1 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::clear_cache}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionclear_cache(=null):bool{=newself();return->invalidator()->clear_cache();}/** * Recursively delete files under an allowlisted swap dir via PHP API. * * @since 2.2.0 * @param string $swap_dir Allowlisted directory. * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_swap_dir_files}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctiondelete_swap_dir_files(string):void{Cache_Invalidator::delete_swap_dir_files();}/** * Fallback purge for LiteSpeed/OLS when LSCWP not active (P0). * * Clears allowlisted swap dirs via PHP API and emits * X-LiteSpeed-Purge header. Gated by is_litespeed() and no-op when * has_action(\'litespeed_purge_all\') exists (handled via sync above). * Filterable via wppo_litespeed_swap_purge. * * @since 2.0.0 * @param string|null $url_path URL path or null for all. * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::purge_litespeed_swap_fallback}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionpurge_litespeed_swap_fallback():void{Cache_Invalidator::purge_litespeed_swap_fallback();}/** * Whether a minify-cache directory may be recursively deleted. * * The min dirs live outside the per-domain tree, so * {@see is_path_contained()} does not apply; instead the target must * sit lexically under the plugin-owned min base dir * (`{WP_CONTENT_DIR}/cache/wppo/min/`) and, when resolvable, its * realpath must stay under the resolved base (a symlinked min dir * pointing outside fails closed). Fail closed on any anomaly. * * @since 2.2.0 * @param string $dir Absolute directory candidate. * @return bool True when the recursive delete may proceed. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::is_min_dir_allowed}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionis_min_dir_allowed(string):bool{return->invalidator()->is_min_dir_allowed();}/** * Whether a file path lives under the min tree (for the PHP resolver). * * The min tree (`{WP_CONTENT_DIR}/cache/wppo/min/...`) sits outside * the per-domain prefix, so {@see is_path_contained()} cannot cover * it; this routes min-tree misses through {@see is_min_dir_allowed()} * on the parent dir instead. Fail-closed. Never throws. * * @since 2.2.0 * @param string $path Absolute file candidate. * @return bool True when the path is min-tree-contained. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::is_min_path}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionis_min_path(string):bool{return->invalidator()->is_min_path();}/** * Resolve bounded purge-fallback snapshot limits. * * Defaults come from the `PURGE_FALLBACK_*` constants; the * `wppo_purge_fallback_limits` filter may override any key * (`max_files`, `max_depth`, `max_bytes`, `max_dirs`, * `max_total_bytes`). Fail-closed: missing/invalid values fall back * to the constant defaults, and filter values are clamped to sane * ceilings so a buggy filter cannot force an OOM * (`max_files × max_bytes` is otherwise buffered in memory). Result * is memoized per request. Never throws. * * @since 2.2.0 * @return array{max_files: int, max_depth: int, max_bytes: int, max_dirs: int, max_total_bytes: int} Limits. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::purge_fallback_limits}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionpurge_fallback_limits():array{returnCache_Invalidator::purge_fallback_limits();}/** * Throttled log when the snapshot bounds are hit. * * A full wipe restores only the bounded snapshot (see * {@see purge_fallback_limits()}); without a signal larger sites * would silently lose fallbacks. Reuses the per-day transient * throttle so a wipe storm writes a single row. Never throws. * * @since 2.2.0 * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::log_snapshot_cap}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionlog_snapshot_cap():void{->invalidator()->log_snapshot_cap();}/** * Snapshot domain-tree fallbacks under a directory slated for wipe. * * Domain-tree entry point over {@see snapshot_purge_fallbacks_worker()}. * * @param string $dir Absolute domain directory about to be deleted. * @return array<string, string> Fallback path => file contents. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::snapshot_domain_purge_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionsnapshot_domain_purge_fallbacks(string):array{return->invalidator()->snapshot_domain_purge_fallbacks();}/** * Snapshot min-tree fallbacks under a directory slated for wipe. * * Min-tree entry point over {@see snapshot_purge_fallbacks_worker()}. * * @param string $dir Absolute min directory about to be deleted. * @return array<string, string> Fallback path => file contents. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::snapshot_min_purge_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionsnapshot_min_purge_fallbacks(string):array{return->invalidator()->snapshot_min_purge_fallbacks();}/** * Shared snapshot worker behind the domain/min entry points. * * @param string $dir Absolute directory about to be deleted. * @param callable $is_allowed Containment validator: fn( string $path ): bool. * @return array<string, string> Fallback path => file contents. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::snapshot_purge_fallbacks_worker}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionsnapshot_purge_fallbacks_worker(string,callable):array{return->invalidator()->snapshot_purge_fallbacks_worker(,);}/** * Snapshot last-good fallback files under a directory slated for wipe. * * Backward-compatible wrapper over the split domain/min entry * points (kept for existing callers/tests): delegates to * {@see snapshot_domain_purge_fallbacks()} or * {@see snapshot_min_purge_fallbacks()} based on $min_tree. * Never throws. * * @param string $dir Absolute directory about to be deleted. * @param bool $min_tree Whether $dir lives under the min tree. * @return array<string, string> Fallback path => file contents. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::snapshot_purge_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionsnapshot_purge_fallbacks(string,bool=false):array{return->invalidator()->snapshot_purge_fallbacks(,);}/** * Retain live bases to sibling fallbacks before a full-tree wipe. * * A full wipe otherwise snapshots only the previous fallback * generation (or nothing on first wipe), defeating the last-good * guarantee. This bounded pre-pass copies each live `.css`/`.js` * base to its sibling fallback via the shared * {@see Util::retain_purge_fallback_file()} helper so the snapshot * that follows captures the current generation. Uses the same * limits (max_dirs walk budget) and never throws. * * @param string $dir Absolute directory about to be deleted. * @param callable $is_allowed Containment validator: fn( string $path ): bool. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::retain_live_bases_for_wipe}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionretain_live_bases_for_wipe(string,callable):void{->invalidator()->retain_live_bases_for_wipe(,);}/** * Restore domain-tree fallbacks after a full-cache wipe. * * @param array<string, string> $snapshot Path => contents from {@see snapshot_purge_fallbacks()}. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::restore_purge_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionrestore_purge_fallbacks(array):void{->invalidator()->restore_purge_fallbacks();}/** * Restore min-tree fallbacks after a full-cache wipe. * * @param array<string, string> $snapshot Path => contents from {@see snapshot_purge_fallbacks()}. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::restore_min_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionrestore_min_fallbacks(array):void{->invalidator()->restore_min_fallbacks();}/** * Shared restore worker behind the domain/min entry points. * * Recreates parent directories via {@see Util::prepare_cache_dir()} * and rewrites each captured fallback. Skips entries that fail the * basename allowlist or the caller-supplied containment check so a * snapshot can never plant files outside its tree. Never throws. * * @param array<string, string> $snapshot Path => contents. * @param callable $is_allowed Containment validator: fn( string $path ): bool. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::restore_snapshot}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionrestore_snapshot(array,callable):void{->invalidator()->restore_snapshot(,);}/** * Stage a snapshot to a temp dir outside the wipe tree. * * The in-memory snapshot alone is lost on a fatal/OOM/timeout * between `delete()` and restore — exactly the outage the fallback * exists to prevent. Spilling each entry to a temp file before the * delete means the bytes survive the wipe even if this request * dies (a later wipe restores from its own fresh snapshot; stale * temp files are always cleaned up after a successful restore). * Returns original-path => temp-path. Never throws. * * @param array<string, string> $snapshot Path => contents. * @return array<string, string> Original path => staged temp path. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::stage_snapshot_to_temp}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionstage_snapshot_to_temp(array):array{return->invalidator()->stage_snapshot_to_temp();}/** * Restore staged temp files back to their original paths. * * Reads each temp file (bounded by the snapshot limits) and * delegates to {@see restore_snapshot()} with the caller-supplied * validator, then removes the staging dir. Callers fall back to * the in-memory snapshot when staging produced nothing. Never * throws. * * @param array<string, string> $staged Original path => staged temp path. * @param callable $is_allowed Containment validator: fn( string $path ): bool. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::restore_staged_snapshot}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionrestore_staged_snapshot(array,callable):void{->invalidator()->restore_staged_snapshot(,);}/** * Delete all cache files. * * @return bool True if successful, false otherwise. * * @since 1.0.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_all_cache_files}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_all_cache_files():bool{return->invalidator()->delete_all_cache_files();}/** * Get the size of the cache. * * @return string * @since 1.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_size():string{return(newself())->capacity()->get_cache_size();}/** * Get detailed cache statistics. * * @return array{size: string, cached_pages: int, last_cleared: string, cache_dir: string} * @since 2.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_stats():array{return(newself())->capacity()->get_cache_stats();}/** * Store the unified cache-stats payload. * * @param array<string,mixed> $unified Unified stats payload. * @param string $stats_key Transient key (multisite-prefixed). * @return void * @since 2.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionstore_cache_stats(array,string):void{(newself())->capacity()->store_cache_stats(,);}/** * List direct cache children for capacity accounting. * * @param string $directory Directory path. * @return array|null Dirlist entries, or null when unavailable. * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functionlist_cache_children(string):?array{return->capacity()->list_cache_children();}/** * Calculate directory size and cached-page count in one walk. * * @param string $directory Directory to scan. * @param int $depth Recursion depth. * @return array{size:int,count:int} Total bytes and index.html count. * @since 2.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functioncalculate_directory_stats(string,int=0):array{return->capacity()->calculate_directory_stats(,);}/** * Calculate the size of a directory. * * @param string $directory Directory to scan. * @param int $depth Recursion depth. * @return int Total size in bytes. * @since 1.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functioncalculate_directory_size(string,int=0):int{return->capacity()->calculate_directory_size(,);}/** * Count cached pages by counting index.html files. * * @param string $directory Directory to scan. * @param int $depth Recursion depth. * @return int Cached page count. * @since 1.9.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functioncount_cached_pages(string,int=0):int{return->capacity()->count_cached_pages(,);}/** * Read bounded-cache cap settings with fail-safe defaults. * * @return array{max_mb:int,warn_ratio:float,enforce:bool,max_files:int,randomized_guard:bool} * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_cap_settings():array{returnCache_Capacity::get_cache_cap_settings();}/** * Get total static-cache bytes and file count in one walk. * * @return array{bytes:int,files:int} Bytes and file count. * @since 2.3.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_bytes_and_files():array{return(newself())->capacity()->get_cache_bytes_and_files();}/** * Get total static-cache size in bytes. * * @return int Bytes used, or zero on failure. * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_size_bytes():int{return(newself())->capacity()->get_cache_size_bytes();}/** * Get total cached-page file count. * * @return int File count, or zero on failure. * @since 2.3.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_file_count():int{return(newself())->capacity()->get_cache_file_count();}/** * Whether an asset URL carries randomized per-request query churn. * * @param string $src Asset src URL. * @return bool Whether the query looks randomized. * @since 2.3.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionis_randomized_query_asset(string):bool{returnCache_Capacity::is_randomized_query_asset();}/** * Get warn-before-enforce cache capacity status. * * @return array{bytes:int,cap_bytes:int,warn_bytes:int,state:string,enforce:bool,max_mb:int,files:int,cap_files:int,warn_files:int} * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_cap_status():array{return(newself())->capacity()->get_cache_cap_status();}/** * Throttled warn-before-enforce cap check after a cache write. * * @return void * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionmaybe_enforce_cache_cap():void{(newself())->capacity()->maybe_enforce_cache_cap();}/** * Evict oldest cached pages until the byte target is freed. * * @param int $bytes_to_free Minimum bytes to reclaim. * @return int Bytes actually freed. * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionevict_oldest_cache_entries(int):int{return(newself())->capacity()->evict_oldest_cache_entries();}/** * Evict oldest cached pages until the file-count target is met. * * @param int $files_to_free Minimum entries to remove. * @return int Entries actually removed. * @since 2.3.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionevict_oldest_cache_files_by_count(int):int{return(newself())->capacity()->evict_oldest_cache_files_by_count();}/** * Collect index.html entries with mtime and size for oldest eviction. * * @param string $directory Directory to scan. * @param int $depth Recursion depth. * @param array $out Accumulator passed by reference. * @return array<int,array{path:string,mtime:int,size:int}> Collected entries. * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functioncollect_cache_entries_by_age(string,int=0,array&=array()):array{return->capacity()->collect_cache_entries_by_age(,,);}/** * Flush a specific cache group via wp_cache_flush_group(). * * Allows targeted flushing of object cache groups (e.g. wppo_minify_check, * wppo_activity_logs) instead of a full cache flush. * * @since 2.0.0 * * @param string $group The cache group to flush. * @return bool True if the flush succeeded, false if the cache implementation * does not support flush_group or the function is unavailable. */staticfunctionflush_group(string):bool{if(function_exists(\'wp_cache_supports\')){if(!wp_cache_supports(\'flush_group\')){returnfalse;}returnwp_cache_flush_group();}if(function_exists(\'wp_cache_flush_group\')){global;if(isset()&&method_exists(,\'flush_group\')){returnwp_cache_flush_group();}}returnfalse;}/** * Evict legacy WP 6.9 pre-salt query-group cache keys. * * Core\'s 6.9+ single-key-per-group cache leaves unsalted post-queries / * term-queries / comment-queries / user-queries / site-queries / network-queries * keys behind once wp_cache_add_salt() has been called. A full wp_cache_flush() * is the only reliable eviction (flush_group() patterns are salt-prefixed and * miss the legacy unsalted keys). On cores without a persistent object cache * this only flushes the in-memory cache and is harmless. * * @since 1.9.0 * @return bool True if the flush ran (or nothing needed evicting on cores * without the WP 6.9+ salt API), false if the cache API is * unavailable. */staticfunctionflush_legacy_query_cache_keys():bool{if(!function_exists(\'wp_cache_get_salted\')){returntrue;}if(function_exists(\'wp_cache_flush\')){returnwp_cache_flush();}returnfalse;}/** * Flush runtime (in-memory) cache via wp_cache_flush_runtime(). * * Avoids unnecessary persistent cache (Redis/Memcached) flushes when only * in-memory cached data has changed (e.g. admin UI settings). * * @since 1.9.0 * * @return bool True if the flush succeeded, false if the function is * unavailable (WP < 6.0). */staticfunctionflush_runtime():bool{if(function_exists(\'wp_cache_flush_runtime\')){returnwp_cache_flush_runtime();}returnfalse;} $group): }Direct reference to the combine preload URL (ARCH-006 internal bridge). Parameter Type Default Description $groupcombine_state_preload_url():string{return->combine_css_preload_url;}/** * Direct reference to the inline-budget size map (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return array<string,array{size:int,readable:bool}>|null Reference to the live size-map state. */function&combine_state_inline_size_map():?array{return->inline_size_map;}/** * Direct reference to the core-will-inline memo (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return array<string,bool> Reference to the live will-inline memo. */function&combine_state_core_will_inline_memo():array{return->core_will_inline_memo;}/** * Direct reference to the src stat LRU (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return array<string,array{readable:bool,size:int|false}> Reference to the live stat cache. */function&combine_state_src_stat_cache():array{return->src_stat_cache;}/** * Direct reference to the sandbox-effective slice memo (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Per-request lifetime keyed by the * production slice hash, so staged preview output and production * output never share a verdict. Audit note: only `Css_Combine` calls * this (no other runtime or test caller exists). Do not call from new * code; the public visibility exists solely for the extraction bridge. * * @internal * @since 2.4.0 * @return array<string,array> Reference to the live sandbox-slice memo. */function&combine_state_sandbox_effective_file_opt_memo():array{return->sandbox_effective_file_opt_memo;}/** * Direct reference to the sandbox preview-flag memo (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return bool|null Reference to the live preview-flag memo (null = unresolved). */function&combine_state_sandbox_preview_memo(){return->sandbox_preview_memo;}/** * Direct reference to the safe-mode inline-bypass memo (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Per-request lifetime keyed by the * production slice hash. Audit note: only `Css_Combine` calls this * (no other runtime or test caller exists). Do not call from new code; * the public visibility exists solely for the extraction bridge. * * @internal * @since 2.4.0 * @return array<string,bool> Reference to the live safe-mode memo. */function&combine_state_safe_mode_inline_memo():array{return->safe_mode_inline_memo;}/** * Direct reference to the inline-drift flag (ARCH-006 internal bridge). * * Gives {@see Css_Combine} the same live request-state access the * relocated bodies had on `Cache`. Audit note: only `Css_Combine` * calls this (no other runtime or test caller exists). Do not call * from new code; the public visibility exists solely for the * extraction bridge. * * @internal * @since 2.4.0 * @return bool Reference to the live drift flag. */function&combine_state_inline_drift_detected():bool{return->inline_drift_detected;}/** * Filesystem for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return object|false|null The filesystem object, or false on init failure. */functioninvalidator_filesystem():object|false|null{return->get_filesystem();}/** * Path-containment verdict for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @param string $path Absolute file or directory path. * @return bool True when contained. */functioninvalidator_is_path_contained(string):bool{return->is_path_contained();}/** * Cache file path for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @param string|null $url_path The URL path (optional). * @param string $type The file type (default: \'html\'). * @return string The file path. */functioninvalidator_get_file_path(?string=null,string=\'html\'):string{return->get_file_path(,);}/** * Traversal-probe log for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @param string $raw_input The hostile input that was rejected. * @return void */functioninvalidator_log_traversal_probe(string):void{->log_traversal_probe();}/** * Cache root directory for the invalidation/purge service (ARCH-007 internal bridge). * * Direct raw read is intentional: no accessor exists for this property, * so the bridge returns the canonical single-source-of-truth value. * If an accessor is introduced later, route this bridge through it. * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return string Cache root directory. */functioninvalidator_cache_root_dir():string{return->cache_root_dir;}/** * Cache domain for the invalidation/purge service (ARCH-007 internal bridge). * * Direct raw read is intentional: no accessor exists for this property, * so the bridge returns the canonical single-source-of-truth value. * If an accessor is introduced later, route this bridge through it. * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return string Cache domain. */functioninvalidator_domain():string{return->domain;}/** * Cache root URL for the invalidation/purge service (ARCH-007 internal bridge). * * Direct raw read is intentional: no accessor exists for this property, * so the bridge returns the canonical single-source-of-truth value. * If an accessor is introduced later, route this bridge through it. * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return string Cache root URL. */functioninvalidator_cache_root_url():string{return->cache_root_url;}/** * Cache directory slug for the invalidation/purge service (ARCH-007 internal bridge). * * Audit note: only `Cache_Invalidator` calls this (no other runtime or test * caller exists). Do not call from new code; the public visibility * exists solely for the extraction bridge. A `_doing_it_wrong()` guard * is deliberately omitted: it would fire on the legitimate internal * caller every request. * * @internal * @access private * @since 2.4.0 * @return string Cache directory slug (`CACHE_DIR`). */functioninvalidator_cache_dir():string{returnself::CACHE_DIR;}/** * Filesystem for the capacity/accounting service (P3-005 internal bridge). * * @internal * @access private * @since NEXT * @return object|false|null Filesystem object, or false on init failure. */functioncapacity_filesystem():object|false|null{return->get_filesystem();}/** * Cache root for the capacity/accounting service (P3-005 internal bridge). * * @internal * @access private * @since NEXT * @return string Canonical cache root directory. */functioncapacity_cache_root_dir():string{return->cache_root_dir;}/** * Cache domain for the capacity/accounting service (P3-005 internal bridge). * * @internal * @access private * @since NEXT * @return string Canonical cache domain. */functioncapacity_domain():string{return->domain;}/** * Path-containment verdict for capacity eviction (P3-005 internal bridge). * * @internal * @access private * @since NEXT * @param string $path Absolute file or directory path. * @return bool True when the path is contained. */functioncapacity_is_path_contained(string):bool{return->is_path_contained();}/** * Delete a cache entry through the invalidation owner (P3-005 internal bridge). * * Keeps storage/invalidation policy on Cache_Invalidator while capacity * eviction retains the existing sibling-aware deletion behavior. * * @internal * @access private * @since NEXT * @param string $file_path Cache entry path. * @return bool Whether deletion succeeded. */functioncapacity_delete_cache_files(string):bool{return->invalidator()->delete_cache_files();}/** * Start output buffer for static HTML cache (WP < 6.9 fallback). * * Creates a static HTML version of the page if not logged in and not a 404 page. * * Tracked by #829: do not remove until minimum supported WP is raised * to 6.9 (`Requires at least: 6.9`). * * Buffer lifecycle guarantees (audit #888 finding 6): * - the ob callback is Throwable-safe and always returns a string (the * original buffer on failure), so the page can never lose output and * the buffer can always be closed; * - the opened level is tracked and a shutdown safety net (priority 0, * before core\'s wp_ob_end_flush_all() at priority 1) flushes the * buffer exactly once if nothing else closed it. When a third party * opened a deeper buffer the net stays out of the way — their close * cascades into ours. * * This hook only fires on template_redirect (front-end HTML), never in * REST/AJAX/admin contexts, so buffered REST/JSON responses are not a * concern by construction. * * @return void * * @since 1.0.0 */functionstart_output_buffer():void{=false;try{if(class_exists(\'PerformanceOptimise\\Inc\\Main\')&&method_exists(\'PerformanceOptimise\\Inc\\Main\',\'should_use_core_template_buffer\')){=Main::should_use_core_template_buffer();}else{=function_exists(\'wp_should_output_buffer_template_for_enhancement\');}}catch(\\Throwable){unset();=function_exists(\'wp_should_output_buffer_template_for_enhancement\');}if(){return;}if(function_exists(\'wp_should_output_buffer_template_for_enhancement\')){try{if(wp_should_output_buffer_template_for_enhancement()){return;}}catch(\\Throwable){unset();}}if(!->is_cache_allowed_for_current_user()||->is_not_cacheable()){return;}if(null!==->cache_ob_level){return;}=->get_logged_in_role_hash();=->get_cache_file_path(\'html\',);=function()use(){try{=->process_buffer_only();->save_processed_buffer(,);return;}catch(\\Throwable){do_action(\'wppo_debug_log\',\'WPPO page cache buffer processing failed.\',array(\'exception\'=>));return;}};if(!ob_start()){do_action(\'wppo_debug_log\',\'WPPO page cache could not open output buffer\');return;}->cache_ob_level=ob_get_level();if(false===has_action(\'shutdown\',array(,\'maybe_end_output_buffer\'))){add_action(\'shutdown\',array(,\'maybe_end_output_buffer\'),0);}}/** * Shutdown safety net: close the cache output buffer exactly once. * * Runs at shutdown priority 0, before core\'s wp_ob_end_flush_all() * (priority 1). Acts only while the tracked buffer is still the * topmost-open level: when a third party opened a deeper buffer, their * close (or core\'s shutdown flush) cascades into ours and this method * is a no-op. Also serves the \"is_not_cacheable flipped mid-request\" * case: the save decision is made inside the callback, the (processed) * buffer is always flushed to the client. * * @since 2.0.0 * @return void */functionmaybe_end_output_buffer():void{if(null===->cache_ob_level){return;}=->cache_ob_level;->cache_ob_level=null;try{if(ob_get_level()===){ob_end_flush();}}catch(\\Throwable){unset();}}/** * Hoist late-enqueued block stylesheets after the block library (WP 6.9 core parity). * * Core 6.9 hoists late-enqueued block assets into `<head>` behind the * template-enhancement buffer (Trac #43258; 6.9 frontend-performance * field guide). Because this pipeline now runs ON the core buffer * (issue #1386), hoisted styles are already present: this pass is * the fail-open net for markup where they are not: per-block * stylesheet `<link>` tags still sitting after `</head>` (late * `wp_enqueue_style()` calls printed in body/footer) are moved to * directly after the `block-library` stylesheet link, preserving * their relative order so the core cascade (library first, then * per-block overrides) stays intact. * * HTML API only: discovery walks `WP_HTML_Tag_Processor`; the move * itself uses plain string offsets, never regex. Idempotent (a * second pass finds nothing after `</head>` and no-ops) and * fail-open (any unexpected shape returns the input unchanged). * * @since 2.3.0 * * @param string $buffer The HTML buffer. * @return string The HTML with late block styles hoisted, or the input unchanged. */functionhoist_late_block_styles(){try{if(!is_string()||\'\'===){return;}if(!class_exists(\'WP_HTML_Tag_Processor\')){return;}if(false===stripos(,\'</head\')||false===stripos(,\'block-library\')||(false===stripos(,\'wp-block-\')&&false===stripos(,\'/blocks/\'))){return;}=stripos(,\'</head>\');if(false===){return;}=new\\WP_HTML_Tag_Processor();=array();while(->next_tag(array(\'tag_name\'=>\'LINK\'))){=->get_attribute(\'rel\');if(!is_string()||false===stripos(,\'stylesheet\')){continue;}=->get_attribute(\'href\');if(!is_string()||\'\'===){continue;}[]=;}if(empty()){return;}=null;foreach(as){if(false!==stripos(,\'block-library\')){=;break;}}if(null===){return;}=array();=array();foreach(as){if(===){continue;}if(false!==stripos(,\'block-library\')){continue;}if(false===stripos(,\'wp-block-\')&&false===stripos(,\'/blocks/\')){continue;}if(isset([])||isset([])){continue;}if(->is_href_in_head_link(,,)){[]=true;continue;}[]=true;}if(empty()&&empty()){return;}=->find_link_tag_for_href(,,0);if(null===){return;}=array();=array();=array();=array_merge(array_keys(),array_keys());foreach(as){=isset([]);=;=0;while(<50){++;=->find_link_tag_for_href(,,);if(null===){break;}[]=;if(!&&!isset([])){[]=true;[]=[2];}=[1]+1;}if(count()>100){return;}}if(empty()){return;}usort(,staticfunction(,){if([0]===[0]){return0;}return([0]<[0])?-1:1;});foreach(as){if([0]<=[1]){return;}}=substr(,0,[1]+1);.=implode(\'\',);=[1]+1;foreach(as){if([0]<){continue;}.=substr(,,[0]-);=[1]+1;}.=substr(,);return;}catch(\\Throwable){unset();returnis_string()?:\'\';}}/** * Locate the next `<link>` tag containing an href, at/after an offset. * * String-offset companion to the HTML-API discovery in * {@see hoist_late_block_styles()}: occurrences of the href that are * not wrapped in a `<link>` tag (script strings, data attributes, * comments) are skipped, so callers always resolve to a genuine * stylesheet tag. Never uses regex. * * @since 2.3.0 * * @param string $buffer The HTML buffer. * @param string $href The stylesheet href to locate. * @param int $offset Byte offset to search from. * @return array|null Array of [start, end, tag text], or null when no `<link>` tag carries the href. */functionfind_link_tag_for_href(,,){=(int);=0;while(<50){++;=strpos(,,);if(false===){returnnull;}=(>2048)?-2048:0;=substr(,,-);=(\'\'!==)?strrpos(,\'<\'):false;=(false!==)?+:false;=strpos(,\'>\',);if(false===||false===||<=){returnnull;}=substr(,,-+1);if(false!==stripos(,\'<link\')){returnarray(,,);}=+strlen();}returnnull;}/** * Whether an href is carried by a genuine `<link>` tag before `</head>`. * * Tag-verified head membership for {@see hoist_late_block_styles()}: * a bare href substring (script string, preload, comment) before the * head close does not count. Never uses regex. * * @since 2.3.0 * * @param string $buffer The HTML buffer. * @param string $href The stylesheet href to test. * @param int $head_close Byte offset of `</head>`. * @return bool True when a `<link>` tag carries the href inside head. */functionis_href_in_head_link(,,){=->find_link_tag_for_href(,,0);return(null!==&&[0]<);}/** * Process the buffer (image optimisation, minification, CDN rewrite) without saving. * * @param string $buffer The content to be processed. * @return string The processed buffer content. * * @since 2.0.0 */functionprocess_buffer_only(){if(!is_string()){return\'\';}if(\'\'===){return;}if(self::){return;}self::=true;=->hoist_late_block_styles();=->get_image_optimisation();=->maybe_serve_next_gen_images();=->add_delay_load_img();=->add_delay_load_backgrounds();=->lazy_load_videos();=->lazy_render_elements();if(!empty(->options[\'file_optimisation\'][\'hostGoogleFontsLocally\']??false)){=->get_google_fonts()->process_buffer();}if(!empty(->options[\'file_optimisation\'][\'fontMetricFallback\']??false)){=->get_google_fonts()->inject_metric_fallback();}=->options[\'file_optimisation\']??array();=!empty([\'minifyHTML\'])||!empty([\'delayJS\'])||!empty([\'minifyInlineCSS\'])||!empty([\'minifyInlineJS\']);if(){=->minify_buffer();}=->maybe_apply_used_css();=->maybe_apply_cdn();return;}/** * Filter callback for wp_template_enhancement_output_buffer (WP 6.9+). * * Processes the output buffer without saving to cache. Part of the WP 6.9 * template-enhancement buffer path adopted in {@see Main::setup_hooks()}: * wp_template_enhancement_output_buffer (filter at priority 10) + * wp_finalized_template_enhancement_output_buffer (action). Registering the * finalized action automatically opts into the buffer (priority 1000 by * default), which disables response streaming — TTFB increases while TTLB * unchanged; see {@see Main::emit_server_timing_header()} for the intentional * tradeoff (Server-Timing keeps disabled by default, emit only on cache-miss). * Returns filtered output via process_buffer_only(); persistence is handled * by {@see Cache::stash_cache()} / save_processed_buffer() to index.html+.gz+.br. * * @param string $filtered_output The filtered output from previous callbacks. * @param string $output The raw output buffer content. * @return string The processed output buffer. * * @since 2.0.0 */functionprocess_buffer_for_cache(,){if(!is_string()){if(is_string()&&\'\'!==){return;}return\'\';}try{if(!->is_cache_allowed_for_current_user()||->is_not_cacheable()){return;}->current_role_hash=->get_logged_in_role_hash();return->process_buffer_only();}catch(\\Throwable){do_action(\'wppo_debug_log\',\'WPPO page cache buffer processing failed.\',array(\'exception\'=>));if(is_string()&&\'\'!==){return;}if(is_string()&&\'\'!==){return;}return\'\';}}/** * Action callback for wp_finalized_template_enhancement_output_buffer (WP 6.9+). * * Stashes the processed output to the static cache files. Complements * {@see Cache::process_buffer_for_cache()} on wp_template_enhancement_output_buffer * (filter). Registering this finalized action opts into the template-enhancement * buffer (priority 1000 by default) which disables response streaming; TTFB * tradeoff is documented in {@see Main::emit_server_timing_header()} and * {@see Main::setup_hooks()} Server-Timing block. Persists via * save_processed_buffer() to index.html + .gz + .br. * * @param string $output The finalized output buffer content (alias $final). * @return void * * @since 2.0.0 */functionstash_cache(){if(!is_string()||\'\'===){return;}try{if(!->is_cache_allowed_for_current_user()||->is_not_cacheable()){return;}=!empty(->current_role_hash)?->current_role_hash:->get_logged_in_role_hash();=->get_cache_file_path(\'html\',);->save_processed_buffer(,);}catch(\\Throwable){do_action(\'wppo_debug_log\',\'WPPO page cache stash failed.\',array(\'exception\'=>));}}/** * Rewrite local asset URLs via CDN class (LS-410 parity). * * Scans img, script, link, source, and video tags and replaces attribute values that start with the site URL * and contain `/wp-content/` or `/wp-includes/`. The attributes handled are `src`, `href`, `data-src`, * `srcset`, and `data-srcset`. If no CDN is configured the buffer is returned unchanged. * * Delegates to CDN::rewrite_buffer() which handles tag attrs, srcset, inline url() and origin guards. * Constant LITESPEED_BYPASS_CDN short-circuits (LSCWP cdn.cls.php:106). * * @param string $buffer HTML buffer. * @return string The HTML with applicable asset URLs rewritten to the CDN, or the original HTML if no changes were made. * @since 1.2.0 * @since 2.0.0 Added LITESPEED_BYPASS_CDN guard and CDN delegation. */functionmaybe_apply_cdn(string):string{if(defined(\'LITESPEED_BYPASS_CDN\')&&LITESPEED_BYPASS_CDN){return;}if(class_exists(\'PerformanceOptimise\\Inc\\CDN\')){returnCDN::rewrite_buffer();}if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_Integration\')&&!LiteSpeed_Integration::can_apply_cdn()){return;}if(!apply_filters(\'wppo_litespeed_can_cdn\',true)){return;}if(has_filter(\'litespeed_can_cdn\')&&!apply_filters(\'litespeed_can_cdn\',true)){return;}return;}/** * Minify the output buffer. * * @param string $buffer The HTML content to be minified. * @return string The minified HTML content. * * @since 1.0.0 */functionminify_buffer(){if(->should_bypass_for_litespeed()){return;}=newMinify\\HTML(,->options);=->get_minified_html();return;}/** * Record the DONOTCACHEPAGE decision on disk and purge stale static files for the current URL. * * Writes a `.wppo-no-cache` marker file next to the cached HTML so the * advanced-cache.php drop-in (which boots before WordPress) can skip serving * a stale static copy. Runs at most once per request. Best-effort: this only * engages for pages that are actually rendered by WordPress at least once * after the constant is set — a page that is already cached and never re- * rendered stays stale until a cache clear, post invalidation, or the * one-time version-upgrade purge removes it. * * @return void * @since 1.9.0 */functionmaybe_mark_page_not_cacheable():void{if(->no_cache_marker_written){return;}->no_cache_marker_written=true;=->get_cache_file_path(\'html\');if(\'\'===){return;}=trailingslashit(dirname()).\'.wppo-no-cache\';if(!->is_path_contained()){->log_traversal_probe(->url_path);return;}=->get_filesystem();if(!){return;}if(->exists()){return;}if(!->prepare_cache_dir()){return;}=->put_contents(,(string)time(),FS_CHMOD_FILE);if(!){return;}->delete_cache_files();->delete_role_variant_files(dirname());}/** * Whether the current request is a WooCommerce AJAX endpoint. * * Matches the pretty-permalink /wc-ajax/... path segment (decoded, case-insensitive) and the * ?wc-ajax=... query parameter (via $_GET and the raw query string). * A bare substring (e.g. /my-wc-ajax-guide/) intentionally does NOT * match — only the exact path segment or parameter name bypasses * the cache (issue #907 review). * * Shared by is_not_cacheable() and maybe_store_cache() so the * storage layer refuses wc-ajax XHRs even if is_not_cacheable() is * bypassed via the wppo_should_cache_request filter. * * @since 2.0.0 * @return bool True for wc-ajax requests. */functionis_wc_ajax_request():bool{=wp_normalize_path(trim(rawurldecode((string)wp_parse_url(->request_uri,PHP_URL_PATH)),\'/\'));if(preg_match(\'#(^|/)wc-ajax(/|$)#i\',)){returntrue;}if(isset([\'wc-ajax\'])){returntrue;}return!empty([\'QUERY_STRING\'])&&(bool)preg_match(\'/(?:^|&)(wc-ajax)(?:=|&|$)/i\',sanitize_text_field(wp_unslash([\'QUERY_STRING\'])));}/** * Whether the current request targets a WooCommerce Store API route. * * Store API responses (`wc/store`, `wcstore`, `wp-json/wc/store*`, * `wp-json/wcstore*`) are dynamic JSON and must never be cached — * unconditional on the `wooSafeMode` toggle, mirroring wc-ajax. * Fail-open: detection failure returns true (treated as dynamic, never cached) and * the broader Woo guards still apply. * * @since 2.0.0 * @return bool True for Store API requests. */functionis_woo_store_api_request():bool{=wp_normalize_path(trim(rawurldecode((string)wp_parse_url(->request_uri,PHP_URL_PATH)),\'/\'));if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_store_api_request\')){try{returnWoo_Detect::is_woo_store_api_request();}catch(\\Throwable){unset();returntrue;}}if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_store_api_path\')){try{if(Woo_Detect::is_woo_store_api_path()){returntrue;}}catch(\\Throwable){unset();}try{=isset([\'rest_route\'])?sanitize_text_field(wp_unslash([\'rest_route\'])):\'\';if(\'\'!==&&Woo_Detect::is_woo_store_api_path()){returntrue;}}catch(\\Throwable){unset();}}if((bool)preg_match(\'#(^|/)(?:wc/store|wcstore|wp-json/wc/store|wp-json/wcstore)(/|$)#i\',\'/\'.)){returntrue;}=isset([\'rest_route\'])?sanitize_text_field(wp_unslash([\'rest_route\'])):\'\';if(\'\'!==&&(bool)preg_match(\'#(^|/)(?:wc/store|wcstore|wp-json/wc/store|wp-json/wcstore)(/|$)#i\',\'/\'.ltrim(,\'/\'))){returntrue;}return!empty([\'QUERY_STRING\'])&&(bool)preg_match(\'#rest_route=[^&]*(?:wc/store|wcstore)#i\',rawurldecode(sanitize_text_field(wp_unslash([\'QUERY_STRING\']))));}/** * Whether the current request should be excluded from static cache due to WooCommerce safe mode. * * Safe-by-default exclusions for WooCommerce: cart/checkout/account, * wc-ajax, add-to-cart, and Woo session/cart cookies. Fail-open: any * detection failure treats the page as non-cacheable (never fatal). When * Woo symbols are missing the conditional-function branch is skipped but * URI/cookie guards remain so hardcoded cart/checkout slugs stay safe even * on non-Woo installs (legacy behaviour preserved, 0 queries). * * @since 2.0.0 * @return bool True when the request is Woo-excluded (not cacheable). */functionis_woo_excluded():bool{try{if(->is_woo_store_api_request()){returntrue;}}catch(\\Throwable){unset();}if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_safe_mode_enabled\')){try{if(!Woo_Detect::is_woo_safe_mode_enabled(is_array(->options)?->options:null)){returnfalse;}}catch(\\Throwable){unset();returntrue;}}elseif(isset(->options[\'cache_settings\'][\'wooSafeMode\'])&&false===->options[\'cache_settings\'][\'wooSafeMode\']){returnfalse;}try{=false;if(function_exists(\'is_wc_endpoint_url\')){try{if(is_wc_endpoint_url()){=true;}}catch(\\Throwable){unset();=true;}}=function_exists(\'is_cart\')||function_exists(\'is_checkout\')||function_exists(\'is_account_page\')||function_exists(\'is_woocommerce\')||class_exists(\'WooCommerce\',false);if(!&&){if(function_exists(\'is_cart\')&&is_cart()){=true;}elseif(function_exists(\'is_checkout\')&&is_checkout()){=true;}elseif(function_exists(\'is_account_page\')&&is_account_page()){=true;}}if(!){=wp_parse_url(->request_uri,PHP_URL_PATH);=wp_normalize_path(trim(rawurldecode((string)),\'/\'));=false;if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_dynamic_path\')){try{=Woo_Detect::is_woo_dynamic_path();}catch(\\Throwable){unset();=true;}}else{=(bool)preg_match(\'#^/(?:cart|checkout|my-account)(?:/|$)#i\',\'/\'.);}if(){=true;}}if(!){if(!empty([\'woocommerce_items_in_cart\'])||!empty([\'woocommerce_cart_hash\'])){=true;}}if(!&&!empty()&&is_array()){=function_exists(\'wp_unslash\')?wp_unslash():;foreach(as=>){=(string);if(function_exists(\'sanitize_key\')){=sanitize_key();}if(0===strpos(,\'wp_woocommerce_session_\')&&!empty()){=true;break;}}}if(!){if(->is_wc_ajax_request()){=true;}}if(!){if(isset([\'add-to-cart\'])){=true;}elseif(!empty([\'QUERY_STRING\'])&&preg_match(\'/(?:^|&)(add-to-cart)(?:=|&|$)/i\',sanitize_text_field(wp_unslash([\'QUERY_STRING\'])))){=true;}}if(!){try{if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_faceted_query\')){=(string)wp_parse_url(->request_uri,PHP_URL_QUERY);if(\'\'===&&!empty([\'QUERY_STRING\'])){=sanitize_text_field(wp_unslash([\'QUERY_STRING\']));}if(\'\'!==&&Woo_Detect::is_woo_faceted_query()){=true;}}}catch(\\Throwable){unset();=true;}}if(!){returnfalse;}if(function_exists(\'has_filter\')&&has_filter(\'wppo_woo_cacheable\')){=(bool)apply_filters(\'wppo_woo_cacheable\',false,->request_uri);if(){returnfalse;}}returntrue;}catch(\\Throwable){unset();returntrue;}}/** * Whether the current response carries a Set-Cookie header. * * Commerce-safety store refusal (issue #1307): a response carrying * Set-Cookie (Woo session, auth, consent) is per-visitor dynamic and * must never be persisted to the static file cache. The pre-boot * drop-in cannot inspect response headers at serve time, so this * write-path check is the enforcement point. Cheap: one * function_exists plus one headers_list scan, only on commerce * relevant write attempts after the early-out guards. Fail-open: * any detection failure returns true (refuse the store, dynamic), * never fatal. * * @since 2.2.0 * @return bool True when a Set-Cookie response header is present. */functionhas_set_cookie_response_header():bool{try{if(!function_exists(\'headers_list\')){returnfalse;}foreach(headers_list()as){if(0===stripos((string),\'set-cookie:\')){returntrue;}}returnfalse;}catch(\\Throwable){unset();returntrue;}}/** * Whether the current request is an admin/editor-preview context. * * Unconditional static-cache bypass (issue #1097): wp-admin / login / * admin-ajax paths, `is_admin()`, `is_preview()`, * `is_customize_preview()`, Elementor preview mode, AJAX/REST/JSON, * and builder/core preview query params via * `Util::is_editor_preview_request()`. No re-allow filter by design: * editor output must never be written to or served from the static * cache. Fail-open: any detection failure bypasses the cache. * * @since 2.2.0 * @return bool True when the request must bypass the cache. */functionis_editor_preview_excluded():bool{if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'is_editor_preview_request\')){try{returnUtil::is_editor_preview_request();}catch(\\Throwable){unset();returntrue;}}try{if(function_exists(\'is_admin\')){try{if(is_admin()){returntrue;}}catch(\\Throwable){unset();returntrue;}}if(function_exists(\'is_preview\')){try{if(is_preview()){returntrue;}}catch(\\Throwable){unset();returntrue;}}if(function_exists(\'is_customize_preview\')){try{if(is_customize_preview()){returntrue;}}catch(\\Throwable){unset();returntrue;}}=wp_normalize_path(trim(rawurldecode((string)wp_parse_url(->request_uri,PHP_URL_PATH)),\'/\'));if((bool)preg_match(\'#(^|/)(?:wp-admin|wp-login\\.php|admin-ajax\\.php)(/|$)#i\',\'/\'.)){returntrue;}=class_exists(\'PerformanceOptimise\\Inc\\Util\')?Util::EDITOR_PREVIEW_PARAMS:array(\'elementor-preview\',\'et_fb\',\'et_pb_preview\',\'vc_action\',\'vc_editable\',\'bricks\',\'preview\',\'preview_id\',\'customize_changeset_uuid\',\'customizer\');foreach(as){if(isset([])){returntrue;}}returnfalse;}catch(\\Throwable){unset();returntrue;}}/** * Check if the page is not cacheable. * * Note: for pages opted out via the DONOTCACHEPAGE constant this also records * the decision on disk (see maybe_mark_page_not_cacheable()). This coupling is * intentional: every render of an opted-out page runs through this predicate * before any buffer/storage path can react, so it is the only reliable place * to write the marker the drop-in checks. Such pages also skip output-buffer * optimisations, matching how every other non-cacheable page behaves. * * @return bool * * @since 1.0.0 */functionis_not_cacheable():bool{if(\'\'===->cache_root_dir){returntrue;}if(empty(->domain)){returntrue;}if(->host_mismatch){returntrue;}if(defined(\'DONOTCACHEPAGE\')&&DONOTCACHEPAGE){->maybe_mark_page_not_cacheable();returntrue;}try{if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'has_uncacheable_query\')&&Util::has_uncacheable_query()){returntrue;}}catch(\\Throwable){unset();returntrue;}/** * Filters whether the current request should be cached. * * Placed after the DONOTCACHEPAGE constant check so the constant * always wins even if the filter returns true. Return false to skip * ob_start and cache storage. * * @since 2.0.0 * * @param bool $should_cache Whether the request should be cached. Default true. * @param string $request_uri The request URI. * @param bool $is_mobile Whether the request is from a mobile device. * @param bool $is_logged_in Whether the user is logged in. */=function_exists(\'wp_is_mobile\')?wp_is_mobile():false;=function_exists(\'is_user_logged_in\')?is_user_logged_in():false;=(bool)apply_filters(\'wppo_should_cache_request\',true,->request_uri,,);if(!){returntrue;}=wp_parse_url(->request_uri,PHP_URL_PATH);=wp_normalize_path(trim(rawurldecode((string)),\'/\'));if(false!==strpos(,\"\\0\")||false!==strpos(,\'..\')){returntrue;}if(function_exists(\'is_feed\')&&is_feed()){returntrue;}if(preg_match(\'/(?:sitemap[^\\/]*\\.xml|wp-sitemap[^\\/]*\\.xml|\\.xml)$/i\',)){returntrue;}try{if(->is_editor_preview_excluded()){returntrue;}}catch(\\Throwable){unset();returntrue;}if(->is_woo_store_api_request()){returntrue;}if(->is_woo_excluded()){returntrue;}if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_ESI\',false)){=false;try{if(LiteSpeed_ESI::should_punch_hole(\'cart\')||LiteSpeed_ESI::should_punch_hole(\'checkout\')||LiteSpeed_ESI::should_punch_hole(\'account\')||LiteSpeed_ESI::should_punch_hole(\'adminbar\')){=true;}}catch(\\Throwable){=false;}if(){if(!defined(\'DONOTCACHEPAGE\')){define(\'DONOTCACHEPAGE\',true);}->maybe_mark_page_not_cacheable();returntrue;}}=pathinfo(,PATHINFO_EXTENSION);returnis_404()||!empty();}/** * Get the cache file path based on the URL path. * * @param string $type The file type (default: \'html\'). * @param string $role_hash Optional role hash for logged-in user cache variant. * @param string $variant Optional variant suffix (e.g. combined-CSS state) baked into the file name. * @return string The cache file path. * * @since 1.0.0 */functionget_cache_file_path(=\'html\',string=\'\',string=\'\'):string{if(\'\'!==&&!preg_match(\'/^[a-z0-9-]{1,32}$/i\',)){->log_traversal_probe();return\'\';}if(\'\'!==&&!preg_match(\'/^[a-z0-9-]{1,32}$/i\',)){->log_traversal_probe();return\'\';}=?\"-{}\":\'\';if(){.=\"-{}\";}if(!preg_match(\'/^[a-z0-9]+$/i\',(string))){->log_traversal_probe((string));return\'\';}=\"index{}.{}\";return->safe_path_for_url(->url_path,);}/** * Get the cache file URL based on the URL path. * * @param string $type The file type (default: \'html\'). * @param string $variant Optional variant suffix baked into the file name. * @return string The cache file URL. * * @since 1.0.0 */functionget_cache_file_url(=\'html\',string=\'\'):string{if(\'\'===->cache_root_url){return\'\';}if(\'\'!==&&!preg_match(\'/^[a-z0-9-]{1,32}$/i\',)){->log_traversal_probe();return\'\';}if(!preg_match(\'/^[a-z0-9]+$/i\',(string))){->log_traversal_probe((string));return\'\';}=?\"-{}\":\'\';=\"index{}.{}\";=->safe_path_for_url(->url_path,);if(\'\'===){return\'\';}=(\'\'===->url_path?:\"{->url_path}/{}\");return\"{->cache_root_url}/{->domain}/{}\";}/** * Apply used-CSS to the buffer if the setting is enabled. * * @param string $buffer The HTML buffer. * @return string The processed buffer. * * @since 1.9.0 */functionmaybe_apply_used_css(string):string{=newUsed_CSS(->options);return->process_buffer();}/** * Prepare the cache directory for storing files. * * @return bool True if successful, false otherwise. * * @since 1.0.0 */functionprepare_cache_dir():bool{=->safe_path_for_url(->url_path,\'index.html\');if(\'\'===){returnfalse;}if(function_exists(\'dirname\')){=dirname();}else{=\"{->cache_root_dir}/{->domain}/\".(\'\'===->url_path?\'\':\"/{->url_path}\");}if(!->is_path_contained(trailingslashit())){->log_traversal_probe(->url_path);returnfalse;}if(!Util::prepare_cache_dir()){returnfalse;}if(!->is_path_contained(trailingslashit())){->log_traversal_probe(->url_path);returnfalse;}returntrue;}/** * Atomically write contents to a file via tmp+rename. * * Writes to a temporary sibling file in the same directory and then * atomically moves it to the final path, preventing readers from * observing a partially-written cache file during stampede writes. * The final path must originate from {@see safe_path_for_url()}; the * containment pre-check below is defense-in-depth on the resolved path. * * @since 2.0.0 * @param string $path Final file path. * @param string $contents File contents. * @return bool True on success. */functionatomic_put_contents(string,string):bool{if(\'\'===||!->is_path_contained()){->log_traversal_probe();returnfalse;}=->get_filesystem();if(!){returnfalse;}returnUtil::atomic_file_put_contents(,,);}/** * Save cache files with optional gzip compression. * * The file path must originate from {@see safe_path_for_url()} (via * `get_cache_file_path()`); the containment guard below plus the * `atomic_put_contents()` re-check on the base and `.gz`/`.br` * siblings are defense-in-depth on resolved paths. * * @param string $buffer The content to save. * @param string $file_path The file path for saving. * @param string $type The file type (default: \'html\'). * @return void * * @since 2.0.0 */functionsave_cache_files(,,=\'html\'):void{if(\'\'===(string)||!->is_path_contained((string))){->log_traversal_probe((string));return;}if(\'html\'===&&!->maybe_store_cache()){return;}if(\'html\'===){=Util::cached_home_url(->request_uri);=apply_filters(\'wppo_cache_page_html\',,);}=.\'.gz\';=.\'.br\';=->get_filesystem();if(!){return;}=Util::transient_key(\'wppo_cache_write_\'.md5());=Util::generate_stampede_owner();=Util::stampede_lock_ttl();if(!Util::acquire_stampede_lock(,,)){return;}try{->atomic_put_contents(,);if(function_exists(\'gzencode\')){=gzencode(,9);if(false!==){->atomic_put_contents(,);}}if(\'html\'===){=false;if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_Integration\')&&method_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_Integration\',\'is_brotli_enabled\')){=LiteSpeed_Integration::is_brotli_enabled();}else{=Util::get_settings();=!empty([\'litespeed_integration\'][\'enableBrotli\']);=extension_loaded(\'brotli\')||function_exists(\'brotli_compress\');=&&;/** * Filter whether brotli generation is enabled (fallback). * * @since 2.0.0 * @param bool $use_brotli Whether brotli is enabled. */=(bool)apply_filters(\'wppo_litespeed_brotli\',);}if(&&function_exists(\'brotli_compress\')){try{=brotli_compress(,4,0);if(false!==&&is_string()){->atomic_put_contents(,);}}catch(\\Throwable){unset();}}}if(\'html\'===){->delete_cache_files(trailingslashit(dirname()).\'.wppo-no-cache\');}}finally{Util::release_stampede_lock(,);}if(\'html\'===){try{self::maybe_enforce_cache_cap();}catch(\\Throwable){unset();}}}/** * Save processed buffer with filesystem guard (shared by legacy and WP 6.9+ paths). * * The file path must originate from {@see safe_path_for_url()}; the * containment guard below is defense-in-depth on the resolved path. * * @param string $buffer The processed buffer content. * @param string $file_path The file path for saving. * @return void * * @since 2.0.0 */functionsave_processed_buffer(string,string):void{if(\'\'===||!->is_path_contained()){->log_traversal_probe();return;}if(!->get_filesystem()||!->prepare_cache_dir()){return;}->save_cache_files(,);}/** * Determine if cache storage is allowed. * * @return bool True if cache can be stored, false otherwise. * * @since 1.0.0 */functionmaybe_store_cache(){if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_ESI\',false)){try{if(LiteSpeed_ESI::should_punch_hole(\'cart\')||LiteSpeed_ESI::should_punch_hole(\'checkout\')||LiteSpeed_ESI::should_punch_hole(\'account\')||LiteSpeed_ESI::should_punch_hole(\'adminbar\')||LiteSpeed_ESI::should_punch_hole(\'nonce\')){if(defined(\'DONOTCACHEPAGE\')&&DONOTCACHEPAGE){->maybe_mark_page_not_cacheable();}elseif(!defined(\'DONOTCACHEPAGE\')){define(\'DONOTCACHEPAGE\',true);->maybe_mark_page_not_cacheable();}returnfalse;}}catch(\\Throwable){unset();}}if(class_exists(\'PerformanceOptimise\\Inc\\LiteSpeed_Integration\')&&LiteSpeed_Integration::is_litespeed()&&!LiteSpeed_Integration::is_wppo_cache_owner()){if(->is_not_cacheable()){if(has_action(\'litespeed_control_set_nocache\')){do_action(\'litespeed_control_set_nocache\',\'wppo not cacheable (ls owns)\');}elseif(!headers_sent()){header(\'X-LiteSpeed-Cache-Control: no-cache\');}}/** * Filter whether WPPO file cache storage should be bypassed on LiteSpeed. * * @since 2.0.0 * @param bool $bypass Whether to bypass file cache. */=(bool)apply_filters(\'wppo_litespeed_bypass_file_cache\',true);if(){returnfalse;}}if(defined(\'DONOTCACHEPAGE\')&&DONOTCACHEPAGE){->maybe_mark_page_not_cacheable();returnfalse;}if(empty(->domain)||->host_mismatch||->path_rejected||false!==strpos(->url_path,\"\\0\")||false!==strpos(->url_path,\'..\')){returnfalse;}try{if(->is_editor_preview_excluded()){returnfalse;}}catch(\\Throwable){unset();returnfalse;}try{if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'has_uncacheable_query\')){if(Util::has_uncacheable_query()){returnfalse;}=isset([\'QUERY_STRING\'])?(string)[\'QUERY_STRING\']:\'\';if(function_exists(\'wp_unslash\')){=wp_unslash();}if(function_exists(\'sanitize_text_field\')){=sanitize_text_field();}if(\'\'!==trim()){returnfalse;}}}catch(\\Throwable){unset();returnfalse;}if(->is_wc_ajax_request()){returnfalse;}if(->is_woo_store_api_request()){returnfalse;}try{if(class_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\')&&method_exists(\'PerformanceOptimise\\Inc\\Woo_Detect\',\'is_woo_faceted_query\')){=(string)wp_parse_url(->request_uri,PHP_URL_QUERY);if(\'\'===&&!empty([\'QUERY_STRING\'])&&function_exists(\'wp_unslash\')&&function_exists(\'sanitize_text_field\')){=sanitize_text_field(wp_unslash([\'QUERY_STRING\']));}if(\'\'!==&&Woo_Detect::is_woo_faceted_query()){returnfalse;}}}catch(\\Throwable){unset();returnfalse;}try{if(->has_set_cookie_response_header()){returnfalse;}}catch(\\Throwable){unset();returnfalse;}if(->is_woo_excluded()){returnfalse;}if(!empty(->options[\'preload_settings\'][\'enablePreloadCache\'])){if(!empty(->options[\'preload_settings\'][\'excludePreloadCache\'])){=Util::process_urls(->options[\'preload_settings\'][\'excludePreloadCache\']);=->request_uri;=wp_parse_url(Util::cached_home_url(),PHP_URL_PATH)??\'\';if(&&\'/\'!==&&0===strpos(,)){=substr(,strlen());}=Util::cached_home_url();if(Util::is_url_excluded(,)){returnfalse;}}}returntrue;}/** * Whether current page request is cacheable (public wrapper for LiteSpeed). * * Renamed from is_request_cacheable() (issue #905) to avoid confusion * with LiteSpeed_Integration::is_request_cacheable(), which has * different semantics (adds query-string + preload-exclusion gates on * top of this predicate). Mirrors is_not_cacheable() for external * callers (e.g. LiteSpeed header emission) without exposing private * internals. Cheap — creates no I/O beyond what is_not_cacheable() * already does. * * @since 2.0.0 * @return bool True if cacheable. */functionis_page_cacheable():bool{return!->is_not_cacheable();}/** * Invalidate dynamic static HTML cache for a specific page and global archives. * * @param int $page_id The page ID. * @return void * * @since 1.0.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::invalidate_dynamic_static_html}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functioninvalidate_dynamic_static_html():void{->invalidator()->invalidate_dynamic_static_html();}/** * Invalidate the static HTML cache for a single post URL only. * * Used by the per-page delay kill-switch (#1037) so toggling * `_wppo_delay_disabled` takes effect on that URL without a full purge * and without the home/archive fan-out of * {@see invalidate_dynamic_static_html()}: only the post permalink\'s * `index.html` (+ gzip/brotli variants, role variants, no-cache marker, * and css/used-css sidecars) is deleted. Multisite-safe: per-site * `get_permalink()` plus domain-based `get_file_path()`, so no * cross-site leakage. Fail-open: any failure is swallowed — callers must * never fatal a meta save. No new WP/PHP APIs; safe on WP 6.2+ / PHP 8.2+. * * Bulk callers (e.g. Elementor bulk regen, issue #1259) pass * $bump_stats=false and rely on the deferred full purge — which bumps * once — instead of paying 6x delete_transient + get_option + * update_option per post inline. * * @since 2.0.0 * @param int $page_id Post ID whose single URL cache must be purged. * @param bool $bump_stats Whether to bump dashboard stats (6x transient * deletes + option write). Default true. * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::invalidate_single_static_html}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functioninvalidate_single_static_html(int,bool=true):void{->invalidator()->invalidate_single_static_html(,);}/** * Surgically invalidate cache for a WooCommerce product, order, or coupon. * * Purges only the object\'s own permalink path (+ css/used-css sidecars) * plus, for products, its product-category/tag archive paths and the * shop page path. Never purges the home page, never calls * clear_cache() (no full-cache wipe), and never schedules preload for * Woo-excluded permalinks. Multisite-safe: per-site get_permalink() + * domain-based get_file_path(), no cross-site purge. * * @since 2.0.0 * @param int $object_id Woo object (product/order/coupon) ID. * @param string $kind Object kind: \'product\', \'order\', or \'coupon\'. * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::invalidate_woo_object}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functioninvalidate_woo_object(int,string):void{->invalidator()->invalidate_woo_object(,);}/** * Whether a traversal probe has been logged this request. * * Rate-limits activity-log writes so a hostile crawler cannot flood * the log table with one entry per request path probe. * * @since 2.0.0 * @var bool */staticbool=false;/** * Sanitize a URL path for cache file mapping. * * Uses only the PHP_URL_PATH component, applies exactly one * rawurldecode pass (single-decode semantics: `%252e` stays encoded * on disk and is never re-decoded), then rejects null bytes, any * remaining `..` segments, and Windows drive prefixes. Returns an * empty string for hostile or empty input. * * Delegates to the shared {@see Util::sanitize_cache_url_path()} * helper so every file-writing surface normalizes identically. * * @since 2.0.0 * @since 2.2.0 Added the optional $allowed_host foreign-host refusal. * @param string|null $url_path Raw URL path or URL. * @param string|null $allowed_host Optional canonical host; threaded to the shared helper so * absolute-form callers refuse foreign hosts. * @return string Sanitized relative path or empty string. */staticfunctionsanitize_cache_url_path(?string,?string=null):string{returnUtil::sanitize_cache_url_path(,);}/** * Whether an absolute path stays inside the cache tree. * * Dual-prefix containment: the normalized path must start with both * the cache root and the per-domain directory (trailing-slash aware * so `wppo-evil` never prefix-matches `wppo`). Empty root or domain * fails closed. Since NEXT the check is symlink-aware: the resolved * target must also pass {@see Util::is_realpath_contained()} so a * symlink planted inside the cache tree cannot redirect a write * outside the root (CVE-2026-18051 class). On containment failure * callers skip the write and serve dynamically uncached. * * @since 2.0.0 * @since 2.2.0 Added realpath symlink containment. * @param string $path Absolute file or directory path. * @return bool True when contained. */functionis_path_contained(string):bool{try{returnUtil::validate_cache_write_path(->cache_root_dir,->domain,);}catch(\\Throwable){unset();returnfalse;}}/** * Single choke-point mapping a URL path + leaf filename to a contained absolute path. * * The ONLY function that maps a URL path + leaf filename to an absolute * filesystem path under `wp-content/cache/wppo/{domain}/{path}/`. Every * cache write and purge funnels through this gate: `get_cache_file_path()`, * `get_cache_file_url()`, `get_file_path()`, and `prepare_cache_dir()` * delegate here, while `atomic_put_contents()`, `save_cache_files()`, * `save_processed_buffer()`, `delete_cache_files()`, * `delete_no_cache_marker()`, `delete_role_variant_files()`, and the * `clear_cache()` single-page branch re-check containment via * {@see is_path_contained()} as defense-in-depth on already-resolved * paths. Returns an empty string on any rejection; callers fail open * (serve dynamic/uncached) and never touch the filesystem. `.htaccess` * writers are never called from this path. * * Internal steps (single place): fail-closed on empty root/domain or * a construction-time `path_rejected` flag; leaf-filename allowlist * (`index` plus optional `-[a-z0-9-]{1,32}` suffix groups with an * `[a-z0-9]+` extension, optional `.gz`/`.br` sibling suffix, plus the * explicit `used-css.css` alternative; length * <= 64; no `/`, `\\`, NUL, or `..`); domain allowlist via * `Util::normalize_cache_host()` (fail-closed on `\'\'`); resolution via * `Util::sanitize_cache_path()` (single-decode + NUL/dot-dot/drive/UNC * rejection + dual-prefix containment); final `is_path_contained()` * re-check; `log_traversal_probe()` on reject (skipped for the benign * homepage so `\'\'`/`\'/\'` never logs). * * @since 2.0.0 * @param string $url_path_or_url Raw URL path or URL. * @param string $filename Leaf filename (e.g. `index.html`). * @return string Contained absolute path, or \'\' when refused. */functionsafe_path_for_url(string,string):string{=(string);if(\'\'===->cache_root_dir||\'\'===->domain){return\'\';}if(->path_rejected){return\'\';}if(\'\'===->domain||false!==strpos(->domain,\'/\')||false!==strpos(->domain,\'\\\\\')||false!==strpos(->domain,\'..\')){return\'\';}=(string);if(\'\'===||strlen()>64){->log_traversal_probe();return\'\';}if(false!==strpos(,\'/\')||false!==strpos(,\'\\\\\')||false!==strpos(,\"\\0\")||false!==strpos(,\'..\')){->log_traversal_probe();return\'\';}=false;if(function_exists(\'preg_match\')){=(bool)preg_match(\'/^(?:index(?:-[a-z0-9-]{1,32})*\\.[a-z0-9]+(?:\\.(?:gz|br))?|used-css\\.css(?:\\.(?:gz|br))?)$/i\',);}else{=(\'index.html\'===||\'used-css.css\'===);}if(!){->log_traversal_probe();return\'\';}=\'\';if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'sanitize_cache_path\')){try{=Util::sanitize_cache_path(->cache_root_dir,->domain,,);}catch(\\Throwable){unset();=\'\';}}if(\'\'===){->log_homepage_aware_probe();return\'\';}if(!->is_path_contained()){->log_traversal_probe();return\'\';}return;}/** * Log a rejected path unless it is the benign homepage. * * `Util::sanitize_cache_path()` returns `\'\'` for both the benign * homepage (`\'\'`/`\'/\'`) and hostile inputs; only the latter is a probe. * * @since 2.0.0 * @param string $raw_input The raw input that resolved to \'\'. * @return void */functionlog_homepage_aware_probe(string):void{=null;try{if(function_exists(\'wp_parse_url\')){=wp_parse_url(,PHP_URL_PATH);}elseif(function_exists(\'parse_url\')){=parse_url(,PHP_URL_PATH);}else{=;}}catch(\\Throwable){unset();=;}if(null===||false===){=;}if(\'\'!==trim(trim((string)),\'/\')){->log_traversal_probe();}}/** * Best-effort cross-request throttle check. * * Returns true only when the transient transport positively reports a * stored value. A missing transport, a miss (false/null), or a * throwing transport all count as \"not throttled\" so a broken * throttle can never suppress the log it guards (fail-open toward * logging). * * @since 2.2.0 * @param string $throttle_key Throttle transient key. * @param int $ttl TTL in seconds when recording a fresh hit. * @return bool True when a previous hit is still recorded. */functionis_throttled(string,int):bool{try{if(!function_exists(\'get_transient\')||!function_exists(\'set_transient\')){returnfalse;}=get_transient();if(false!==&&null!==){returntrue;}try{set_transient(,1,);}catch(\\Throwable){unset();}}catch(\\Throwable){unset();}returnfalse;}/** * Log a blocked cache path traversal probe (once per request + throttled across requests). * * The once-per-request static gate alone lets an unauthenticated * crawler insert one wppo_activity_logs row per request (log-table * bloat / DB DoS), so a short-TTL transient gate per probe hash * throttles cross-request repeats (mirroring the * log_inline_budget_drift throttle). Never throws: failures degrade * silently to serving uncached. * * @since 2.0.0 * @param string $raw_input The hostile input that was rejected. * @return void */functionlog_traversal_probe(string):void{if(self::){return;}self::=true;try{if(!class_exists(\'PerformanceOptimise\\Inc\\Log\')){return;}if(class_exists(\'PerformanceOptimise\\Inc\\Util\')&&method_exists(\'PerformanceOptimise\\Inc\\Util\',\'transient_key\')){=Util::transient_key(\'wppo_probe_\'.md5(substr((string),0,64)));=defined(\'HOUR_IN_SECONDS\')?HOUR_IN_SECONDS:3600;if(->is_throttled(,)){return;}}=str_replace(\"\\0\",\'\',(string));if(function_exists(\'sanitize_text_field\')){=sanitize_text_field();}=substr(,0,200);=function_exists(\'__\')?__(\'Blocked cache path traversal probe.\',\'performance-optimisation\'):\'Blocked cache path traversal probe.\';if(\'\'!==){.=\' \'.;}Log::add();}catch(\\Throwable){unset();}}/** * Get the file path for a specific page. * * Hardened against cache-key path traversal (CVE-2026-3129 follow-up): * only the PHP_URL_PATH component is used, exactly one rawurldecode * pass is applied, and null bytes plus `..` segments are rejected. * The resolved path must pass dual-prefix containment before it is * returned; otherwise an empty string is returned and the probe is * logged so the request is served uncached. * * @param string|null $url_path The URL path (optional). * @param string $type The file type (default: \'html\'). * @return string The file path. * * @since 1.1.1 */functionget_file_path(?string=null,string=\'html\'):string{=(string);if(\'used-css\'===){=\'used-css.css\';}else{if(!preg_match(\'/^[a-z0-9]+$/i\',(string))){->log_traversal_probe();return\'\';}=\"index.{}\";}return->safe_path_for_url(,);}/** * Map a derived asset path to its sibling last-good fallback path. * * Thin wrapper over {@see Util::get_purge_fallback_path_for()} so the * purge/miss path can be unit-tested via the Cache surface. Returns * `\'\'` for non CSS/JS paths and for paths that already point at a * fallback file (loop guard). * * @param string $file_path Absolute derived-asset path. * @return string Sibling fallback path, or \'\' when not applicable. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::get_purge_fallback_path}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionget_purge_fallback_path(string):string{returnCache_Invalidator::get_purge_fallback_path();}/** * Retain a last-good fallback copy before a derived file is purged. * * Thin wrapper over {@see Util::retain_purge_fallback_file()} (the * single shared implementation — see also * `Used_CSS::retain_purge_fallback()`). No-op when disabled, for * non CSS/JS paths, on containment failure, or when the base file * is missing/empty. Never throws. * * @param string $file_path The derived file about to be deleted. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::retain_purge_fallback}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionretain_purge_fallback(string):void{->invalidator()->retain_purge_fallback();}/** * Resolve a post-purge miss under the cache path to its fallback. * * Returns `served=true` with a redirect status (default 302 via * {@see purge_fallback_redirect_status()}) and the fallback * path/URL only when every guard holds: the gate is on, the requested path is * contained, it is not itself a fallback file (no loops), the base * file is missing, and a non-empty fallback sibling exists. A single * throttled log entry is written per fallback directory per day so a * sustained miss storm cannot flood the activity log. All other cases * return `served=false` with status 404 (legacy hard-404 preserved, * including when the gate is off). Never throws. * * @param string $requested_path Absolute requested derived-asset path. * @return array{served: bool, status: int, fallback_path: string, location: string} Resolution. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::get_purge_fallback_response}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionget_purge_fallback_response(string):array{return->invalidator()->get_purge_fallback_response();}/** * Resolve the redirect status for a fallback serve. * * Filterable via `wppo_purge_fallback_redirect_status` (default * {@see PURGE_FALLBACK_REDIRECT_STATUS}); invalid values fall back * to the constant. Never throws. * * @since 2.2.0 * @return int Redirect status code. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::purge_fallback_redirect_status}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionpurge_fallback_redirect_status():int{returnCache_Invalidator::purge_fallback_redirect_status();}/** * Build the headers for a fallback redirect (pure, testable). * * The `Cache-Control: no-store` line is deliberate: without it * browsers/proxies may heuristically cache the 302 and keep * redirecting to `fallback.css` after the base regenerates — a * stale-serve window regeneration cannot clear client-side. * * @since 2.2.0 * @param string $location Absolute fallback URL. * @param int $status Redirect status code. * @return array{location: string, status: int, headers: string[]} Headers to send. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::build_purge_fallback_headers}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionbuild_purge_fallback_headers(string,int):array{returnCache_Invalidator::build_purge_fallback_headers(,);}/** * Serve a resolved purge fallback with a redirect. * * Thin sender for {@see get_purge_fallback_response()}: no-op unless * the resolution carries `served=true` with a non-empty location. * Uses `wp_safe_redirect()` when available, `header()` otherwise, * and sends `Cache-Control: no-store` so the redirect itself is * never cached past regeneration. The location is stripped of * literal and encoded CR/LF before sending (defense-in-depth: it is * internally built, but this method is public). Never throws; * terminates the request on success via `exit` (skipped when the * `WPPO_PURGE_FALLBACK_NO_EXIT` test seam is set, returning true). * * @param array{served: bool, status: int, fallback_path: string, location: string} $response Resolver output. * @return bool True when the fallback was served (or would be, under the test seam). * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::serve_purge_fallback_response}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionserve_purge_fallback_response(array):bool{return->invalidator()->serve_purge_fallback_response();}/** * Serve the last-good fallback for Nginx `?wppo_purge_fallback=` misses. * * Paired with the Nginx snippet emitted by * {@see Server_Rules::get_nginx_rules()}: `try_files` falls through to * `index.php?wppo_purge_fallback=$uri` on a miss under the cache path, * and this handler 302s to the sibling fallback when one was retained. * No-op when the gate is off, when the query var is absent, or when no * fallback resolves (legacy 404 flow continues). Never throws. * * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::maybe_serve_purge_fallback}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionmaybe_serve_purge_fallback():void{->invalidator()->maybe_serve_purge_fallback();}/** * Log a purge-fallback serve (global once-per-day throttle). * * Shares the single `wppo_purge_fallback_served` transient via * {@see Util::purge_fallback_should_log()} so a sustained post-purge * miss storm writes one activity-log row per day total (not one per * directory). Fail-open: logging failures never affect serving. * * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::log_purge_fallback}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionlog_purge_fallback():void{->invalidator()->log_purge_fallback();}/** * Delete used-CSS file for a specific file path. * * @param string $file_path The used-css file path. * @return bool True if successful (or not exists), false otherwise. * * @since 1.9.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_used_css_file}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_used_css_file(string):bool{return->invalidator()->delete_used_css_file();}/** * Delete the DONOTCACHEPAGE marker that lives beside a cached HTML file. * * @param string $html_file_path The HTML cache file path whose directory holds the marker. * @return void * * @since 1.9.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_no_cache_marker}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_no_cache_marker(string):void{->invalidator()->delete_no_cache_marker();}/** * Delete cache files for a specific file path. * * The file path must originate from {@see safe_path_for_url()}; the * base plus `.gz`/`.br` sibling containment checks below are * defense-in-depth on resolved paths. * * @param string $file_path The file path. * @return bool True if successful (or not exists), false otherwise. * * @since 1.1.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_cache_files}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_cache_files():bool{return->invalidator()->delete_cache_files();}/** * Delete all index-{hash}.html role-variant cache files in a directory. * * The directory must derive from a {@see safe_path_for_url()}-resolved * path (e.g. `dirname()` of a choke-point result); the directory and * per-file containment checks below are defense-in-depth. * * @param string $dir Directory to scan. * @return void * @since 1.9.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_role_variant_files}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_role_variant_files(string):void{->invalidator()->delete_role_variant_files();}/** * Clear the cache for a specific page or all pages. * * Also flushes any static HTML pages that speculative prerendering * (speculation rules) may have requested and cached: such requests are * ordinary GETs that produce the same per-URL static files (plus their * `.gz` variants and role variants) served to every other visitor, so * the full clear below removes the whole domain directory and the * single-page clear removes the page\'s HTML, gzip, and role-variant * copies. A stale prerendered copy is therefore never served after * invalidation. * * @param string|null $url_path The URL path of the page for which to clear the cache. If null, all cache will be cleared. * @return bool True on success, false on failure. * * @since 1.1.1 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::clear_cache}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionclear_cache(=null):bool{=newself();return->invalidator()->clear_cache();}/** * Recursively delete files under an allowlisted swap dir via PHP API. * * @since 2.2.0 * @param string $swap_dir Allowlisted directory. * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_swap_dir_files}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctiondelete_swap_dir_files(string):void{Cache_Invalidator::delete_swap_dir_files();}/** * Fallback purge for LiteSpeed/OLS when LSCWP not active (P0). * * Clears allowlisted swap dirs via PHP API and emits * X-LiteSpeed-Purge header. Gated by is_litespeed() and no-op when * has_action(\'litespeed_purge_all\') exists (handled via sync above). * Filterable via wppo_litespeed_swap_purge. * * @since 2.0.0 * @param string|null $url_path URL path or null for all. * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::purge_litespeed_swap_fallback}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionpurge_litespeed_swap_fallback():void{Cache_Invalidator::purge_litespeed_swap_fallback();}/** * Whether a minify-cache directory may be recursively deleted. * * The min dirs live outside the per-domain tree, so * {@see is_path_contained()} does not apply; instead the target must * sit lexically under the plugin-owned min base dir * (`{WP_CONTENT_DIR}/cache/wppo/min/`) and, when resolvable, its * realpath must stay under the resolved base (a symlinked min dir * pointing outside fails closed). Fail closed on any anomaly. * * @since 2.2.0 * @param string $dir Absolute directory candidate. * @return bool True when the recursive delete may proceed. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::is_min_dir_allowed}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionis_min_dir_allowed(string):bool{return->invalidator()->is_min_dir_allowed();}/** * Whether a file path lives under the min tree (for the PHP resolver). * * The min tree (`{WP_CONTENT_DIR}/cache/wppo/min/...`) sits outside * the per-domain prefix, so {@see is_path_contained()} cannot cover * it; this routes min-tree misses through {@see is_min_dir_allowed()} * on the parent dir instead. Fail-closed. Never throws. * * @since 2.2.0 * @param string $path Absolute file candidate. * @return bool True when the path is min-tree-contained. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::is_min_path}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionis_min_path(string):bool{return->invalidator()->is_min_path();}/** * Resolve bounded purge-fallback snapshot limits. * * Defaults come from the `PURGE_FALLBACK_*` constants; the * `wppo_purge_fallback_limits` filter may override any key * (`max_files`, `max_depth`, `max_bytes`, `max_dirs`, * `max_total_bytes`). Fail-closed: missing/invalid values fall back * to the constant defaults, and filter values are clamped to sane * ceilings so a buggy filter cannot force an OOM * (`max_files × max_bytes` is otherwise buffered in memory). Result * is memoized per request. Never throws. * * @since 2.2.0 * @return array{max_files: int, max_depth: int, max_bytes: int, max_dirs: int, max_total_bytes: int} Limits. * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::purge_fallback_limits}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */staticfunctionpurge_fallback_limits():array{returnCache_Invalidator::purge_fallback_limits();}/** * Throttled log when the snapshot bounds are hit. * * A full wipe restores only the bounded snapshot (see * {@see purge_fallback_limits()}); without a signal larger sites * would silently lose fallbacks. Reuses the per-day transient * throttle so a wipe storm writes a single row. Never throws. * * @since 2.2.0 * @return void * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::log_snapshot_cap}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionlog_snapshot_cap():void{->invalidator()->log_snapshot_cap();}/** * Snapshot domain-tree fallbacks under a directory slated for wipe. * * Domain-tree entry point over {@see snapshot_purge_fallbacks_worker()}. * * @param string $dir Absolute domain directory about to be deleted. * @return array<string, string> Fallback path => file contents. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::snapshot_domain_purge_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionsnapshot_domain_purge_fallbacks(string):array{return->invalidator()->snapshot_domain_purge_fallbacks();}/** * Snapshot min-tree fallbacks under a directory slated for wipe. * * Min-tree entry point over {@see snapshot_purge_fallbacks_worker()}. * * @param string $dir Absolute min directory about to be deleted. * @return array<string, string> Fallback path => file contents. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::snapshot_min_purge_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionsnapshot_min_purge_fallbacks(string):array{return->invalidator()->snapshot_min_purge_fallbacks();}/** * Shared snapshot worker behind the domain/min entry points. * * @param string $dir Absolute directory about to be deleted. * @param callable $is_allowed Containment validator: fn( string $path ): bool. * @return array<string, string> Fallback path => file contents. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::snapshot_purge_fallbacks_worker}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionsnapshot_purge_fallbacks_worker(string,callable):array{return->invalidator()->snapshot_purge_fallbacks_worker(,);}/** * Snapshot last-good fallback files under a directory slated for wipe. * * Backward-compatible wrapper over the split domain/min entry * points (kept for existing callers/tests): delegates to * {@see snapshot_domain_purge_fallbacks()} or * {@see snapshot_min_purge_fallbacks()} based on $min_tree. * Never throws. * * @param string $dir Absolute directory about to be deleted. * @param bool $min_tree Whether $dir lives under the min tree. * @return array<string, string> Fallback path => file contents. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::snapshot_purge_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionsnapshot_purge_fallbacks(string,bool=false):array{return->invalidator()->snapshot_purge_fallbacks(,);}/** * Retain live bases to sibling fallbacks before a full-tree wipe. * * A full wipe otherwise snapshots only the previous fallback * generation (or nothing on first wipe), defeating the last-good * guarantee. This bounded pre-pass copies each live `.css`/`.js` * base to its sibling fallback via the shared * {@see Util::retain_purge_fallback_file()} helper so the snapshot * that follows captures the current generation. Uses the same * limits (max_dirs walk budget) and never throws. * * @param string $dir Absolute directory about to be deleted. * @param callable $is_allowed Containment validator: fn( string $path ): bool. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::retain_live_bases_for_wipe}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionretain_live_bases_for_wipe(string,callable):void{->invalidator()->retain_live_bases_for_wipe(,);}/** * Restore domain-tree fallbacks after a full-cache wipe. * * @param array<string, string> $snapshot Path => contents from {@see snapshot_purge_fallbacks()}. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::restore_purge_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionrestore_purge_fallbacks(array):void{->invalidator()->restore_purge_fallbacks();}/** * Restore min-tree fallbacks after a full-cache wipe. * * @param array<string, string> $snapshot Path => contents from {@see snapshot_purge_fallbacks()}. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::restore_min_fallbacks}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionrestore_min_fallbacks(array):void{->invalidator()->restore_min_fallbacks();}/** * Shared restore worker behind the domain/min entry points. * * Recreates parent directories via {@see Util::prepare_cache_dir()} * and rewrites each captured fallback. Skips entries that fail the * basename allowlist or the caller-supplied containment check so a * snapshot can never plant files outside its tree. Never throws. * * @param array<string, string> $snapshot Path => contents. * @param callable $is_allowed Containment validator: fn( string $path ): bool. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::restore_snapshot}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionrestore_snapshot(array,callable):void{->invalidator()->restore_snapshot(,);}/** * Stage a snapshot to a temp dir outside the wipe tree. * * The in-memory snapshot alone is lost on a fatal/OOM/timeout * between `delete()` and restore — exactly the outage the fallback * exists to prevent. Spilling each entry to a temp file before the * delete means the bytes survive the wipe even if this request * dies (a later wipe restores from its own fresh snapshot; stale * temp files are always cleaned up after a successful restore). * Returns original-path => temp-path. Never throws. * * @param array<string, string> $snapshot Path => contents. * @return array<string, string> Original path => staged temp path. * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::stage_snapshot_to_temp}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionstage_snapshot_to_temp(array):array{return->invalidator()->stage_snapshot_to_temp();}/** * Restore staged temp files back to their original paths. * * Reads each temp file (bounded by the snapshot limits) and * delegates to {@see restore_snapshot()} with the caller-supplied * validator, then removes the staging dir. Callers fall back to * the in-memory snapshot when staging produced nothing. Never * throws. * * @param array<string, string> $staged Original path => staged temp path. * @param callable $is_allowed Containment validator: fn( string $path ): bool. * @return void * * @since 2.2.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::restore_staged_snapshot}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functionrestore_staged_snapshot(array,callable):void{->invalidator()->restore_staged_snapshot(,);}/** * Delete all cache files. * * @return bool True if successful, false otherwise. * * @since 1.0.0 * Facade proxy (ARCH-007): logic lives in {@see Cache_Invalidator::delete_all_cache_files}. * @since 2.4.0 Proxied to Cache_Invalidator (ARCH-007). */functiondelete_all_cache_files():bool{return->invalidator()->delete_all_cache_files();}/** * Get the size of the cache. * * @return string * @since 1.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_size():string{return(newself())->capacity()->get_cache_size();}/** * Get detailed cache statistics. * * @return array{size: string, cached_pages: int, last_cleared: string, cache_dir: string} * @since 2.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_stats():array{return(newself())->capacity()->get_cache_stats();}/** * Store the unified cache-stats payload. * * @param array<string,mixed> $unified Unified stats payload. * @param string $stats_key Transient key (multisite-prefixed). * @return void * @since 2.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionstore_cache_stats(array,string):void{(newself())->capacity()->store_cache_stats(,);}/** * List direct cache children for capacity accounting. * * @param string $directory Directory path. * @return array|null Dirlist entries, or null when unavailable. * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functionlist_cache_children(string):?array{return->capacity()->list_cache_children();}/** * Calculate directory size and cached-page count in one walk. * * @param string $directory Directory to scan. * @param int $depth Recursion depth. * @return array{size:int,count:int} Total bytes and index.html count. * @since 2.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functioncalculate_directory_stats(string,int=0):array{return->capacity()->calculate_directory_stats(,);}/** * Calculate the size of a directory. * * @param string $directory Directory to scan. * @param int $depth Recursion depth. * @return int Total size in bytes. * @since 1.0.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functioncalculate_directory_size(string,int=0):int{return->capacity()->calculate_directory_size(,);}/** * Count cached pages by counting index.html files. * * @param string $directory Directory to scan. * @param int $depth Recursion depth. * @return int Cached page count. * @since 1.9.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functioncount_cached_pages(string,int=0):int{return->capacity()->count_cached_pages(,);}/** * Read bounded-cache cap settings with fail-safe defaults. * * @return array{max_mb:int,warn_ratio:float,enforce:bool,max_files:int,randomized_guard:bool} * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_cap_settings():array{returnCache_Capacity::get_cache_cap_settings();}/** * Get total static-cache bytes and file count in one walk. * * @return array{bytes:int,files:int} Bytes and file count. * @since 2.3.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_bytes_and_files():array{return(newself())->capacity()->get_cache_bytes_and_files();}/** * Get total static-cache size in bytes. * * @return int Bytes used, or zero on failure. * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_size_bytes():int{return(newself())->capacity()->get_cache_size_bytes();}/** * Get total cached-page file count. * * @return int File count, or zero on failure. * @since 2.3.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_file_count():int{return(newself())->capacity()->get_cache_file_count();}/** * Whether an asset URL carries randomized per-request query churn. * * @param string $src Asset src URL. * @return bool Whether the query looks randomized. * @since 2.3.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionis_randomized_query_asset(string):bool{returnCache_Capacity::is_randomized_query_asset();}/** * Get warn-before-enforce cache capacity status. * * @return array{bytes:int,cap_bytes:int,warn_bytes:int,state:string,enforce:bool,max_mb:int,files:int,cap_files:int,warn_files:int} * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionget_cache_cap_status():array{return(newself())->capacity()->get_cache_cap_status();}/** * Throttled warn-before-enforce cap check after a cache write. * * @return void * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionmaybe_enforce_cache_cap():void{(newself())->capacity()->maybe_enforce_cache_cap();}/** * Evict oldest cached pages until the byte target is freed. * * @param int $bytes_to_free Minimum bytes to reclaim. * @return int Bytes actually freed. * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionevict_oldest_cache_entries(int):int{return(newself())->capacity()->evict_oldest_cache_entries();}/** * Evict oldest cached pages until the file-count target is met. * * @param int $files_to_free Minimum entries to remove. * @return int Entries actually removed. * @since 2.3.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */staticfunctionevict_oldest_cache_files_by_count(int):int{return(newself())->capacity()->evict_oldest_cache_files_by_count();}/** * Collect index.html entries with mtime and size for oldest eviction. * * @param string $directory Directory to scan. * @param int $depth Recursion depth. * @param array $out Accumulator passed by reference. * @return array<int,array{path:string,mtime:int,size:int}> Collected entries. * @since 2.2.0 * @since NEXT Proxied to Cache_Capacity (P3-005). */functioncollect_cache_entries_by_age(string,int=0,array&=array()):array{return->capacity()->collect_cache_entries_by_age(,,);}/** * Flush a specific cache group via wp_cache_flush_group(). * * Allows targeted flushing of object cache groups (e.g. wppo_minify_check, * wppo_activity_logs) instead of a full cache flush. * * @since 2.0.0 * * @param string $group The cache group to flush. * @return bool True if the flush succeeded, false if the cache implementation * does not support flush_group or the function is unavailable. */staticfunctionflush_group(string):bool{if(function_exists(\'wp_cache_supports\')){if(!wp_cache_supports(\'flush_group\')){returnfalse;}returnwp_cache_flush_group();}if(function_exists(\'wp_cache_flush_group\')){global;if(isset()&&method_exists(,\'flush_group\')){returnwp_cache_flush_group();}}returnfalse;}/** * Evict legacy WP 6.9 pre-salt query-group cache keys. * * Core\'s 6.9+ single-key-per-group cache leaves unsalted post-queries / * term-queries / comment-queries / user-queries / site-queries / network-queries * keys behind once wp_cache_add_salt() has been called. A full wp_cache_flush() * is the only reliable eviction (flush_group() patterns are salt-prefixed and * miss the legacy unsalted keys). On cores without a persistent object cache * this only flushes the in-memory cache and is harmless. * * @since 1.9.0 * @return bool True if the flush ran (or nothing needed evicting on cores * without the WP 6.9+ salt API), false if the cache API is * unavailable. */staticfunctionflush_legacy_query_cache_keys():bool{if(!function_exists(\'wp_cache_get_salted\')){returntrue;}if(function_exists(\'wp_cache_flush\')){returnwp_cache_flush();}returnfalse;}/** * Flush runtime (in-memory) cache via wp_cache_flush_runtime(). * * Avoids unnecessary persistent cache (Redis/Memcached) flushes when only * in-memory cached data has changed (e.g. admin UI settings). * * @since 1.9.0 * * @return bool True if the flush succeeded, false if the function is * unavailable (WP < 6.0). */staticfunctionflush_runtime():bool{if(function_exists(\'wp_cache_flush_runtime\')){returnwp_cache_flush_runtime();}returnfalse;}— —
Return: string — Reference to the live preload URL state.
Hooks
Hooks referenced in includes/Cache/class-cache.php: Hook Type Line Notes wppo_debug_logaction 453 — wppo_debug_logaction 455 — wppo_allow_hidden_block_assetfilter 924 — wppo_debug_logaction 2304 — wppo_debug_logaction 2313 — wppo_debug_logaction 2728 — wppo_debug_logaction 2773 — wppo_litespeed_can_cdnfilter 2804 — wppo_woo_cacheablefilter 3119 — wppo_should_cache_requestfilter 3308 @param ×4 wppo_cache_page_htmlfilter 3563 — wppo_litespeed_brotlifilter 3613 @param ×1 wppo_litespeed_bypass_file_cachefilter 3715 @param ×1