Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
85.33% |
64 / 75 |
|
36.36% |
4 / 11 |
CRAP | |
0.00% |
0 / 1 |
| CloudflareFeaturesManager | |
85.33% |
64 / 75 |
|
36.36% |
4 / 11 |
42.56 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
| on_site_capabilities_change | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| get_cookie_value | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
| print_cookie_script | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
3.00 | |||
| maybe_remove_legacy_htaccess_fragment | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
| strip_legacy_block_from_state | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
10.02 | |||
| remove_marker_block | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
| get_htaccess_state | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
| update_htaccess_state | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
| is_cleanup_done | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
| set_cleanup_done | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * CloudflareFeaturesManager |
| 4 | * |
| 5 | * Tracks Cloudflare-related optimization toggles (Polish, Mirage, Fonts) and |
| 6 | * advertises the active set to Cloudflare via a front-end cookie. |
| 7 | * |
| 8 | * The cookie is written client-side (JavaScript) rather than through an |
| 9 | * `.htaccess` `Set-Cookie` response header. A `Set-Cookie` on a front-end |
| 10 | * response makes that response uncacheable to nginx+ and Cloudflare, and the |
| 11 | * old rule emitted it on every cookieless request — so only visitors that |
| 12 | * already held the cookie could ever populate the shared cache. Setting the |
| 13 | * cookie from JS keeps every HTML response cacheable while still presenting the |
| 14 | * cookie on subsequent requests, which is all the Cloudflare edge rules key on. |
| 15 | * |
| 16 | * @package NewfoldLabs\WP\Module\Performance\Cloudflare |
| 17 | * @since 1.0.0 |
| 18 | */ |
| 19 | |
| 20 | namespace NewfoldLabs\WP\Module\Performance\Cloudflare; |
| 21 | |
| 22 | use NewfoldLabs\WP\Module\Performance\Fonts\FontSettings; |
| 23 | use NewfoldLabs\WP\Module\Performance\Images\ImageSettings; |
| 24 | use NewfoldLabs\WP\Module\Htaccess\Api as HtaccessApi; |
| 25 | |
| 26 | /** |
| 27 | * Handles detection and tracking of Cloudflare Polish, Mirage, and Font Optimization. |
| 28 | * |
| 29 | * @since 1.0.0 |
| 30 | */ |
| 31 | class CloudflareFeaturesManager { |
| 32 | |
| 33 | /** |
| 34 | * Cookie name the Cloudflare edge rules look for. |
| 35 | * |
| 36 | * @var string |
| 37 | */ |
| 38 | private const COOKIE_NAME = 'nfd-enable-cf-opt'; |
| 39 | |
| 40 | /** |
| 41 | * Cookie lifetime in seconds (24 hours). |
| 42 | * |
| 43 | * @var int |
| 44 | */ |
| 45 | private const COOKIE_TTL = 86400; |
| 46 | |
| 47 | /** |
| 48 | * Identifier the legacy Cloudflare optimization block is persisted under. |
| 49 | * |
| 50 | * Used only to locate and remove that one block from the htaccess module's saved |
| 51 | * state. No fragment is ever registered under this ID again. |
| 52 | * |
| 53 | * @var string |
| 54 | */ |
| 55 | private const FRAGMENT_ID = 'nfd.cloudflare.optimization.header'; |
| 56 | |
| 57 | /** |
| 58 | * Marker label printed in the BEGIN/END comments of the legacy `.htaccess` block. |
| 59 | * |
| 60 | * On installs migrated to the htaccess "managed marker block" format the block is |
| 61 | * baked into the persisted body and is identifiable only by this label (not by the |
| 62 | * fragment ID), so the cleanup matches on it directly. |
| 63 | * |
| 64 | * @var string |
| 65 | */ |
| 66 | private const MARKER = 'Newfold CF Optimization Header'; |
| 67 | |
| 68 | /** |
| 69 | * Option name where the htaccess module persists its composed state. |
| 70 | * |
| 71 | * Read/written directly (not through the htaccess code) so the cleanup can strip |
| 72 | * the legacy block from the persisted body without any change to the htaccess |
| 73 | * module. This is the documented option name from that module's Options map. |
| 74 | * |
| 75 | * @var string |
| 76 | */ |
| 77 | private const HTACCESS_STATE_OPTION = 'nfd_module_htaccess_saved_state'; |
| 78 | |
| 79 | /** |
| 80 | * Option flag recording that the legacy `.htaccess` block has been removed. |
| 81 | * |
| 82 | * @var string |
| 83 | */ |
| 84 | private const CLEANUP_FLAG = 'nfd_cf_opt_cookie_htaccess_cleaned'; |
| 85 | |
| 86 | /** |
| 87 | * Constructor to register hooks. |
| 88 | * |
| 89 | * @since 1.0.0 |
| 90 | * |
| 91 | * @param mixed $container Optional DI container retained for backwards compatibility. |
| 92 | */ |
| 93 | public function __construct( $container = null ) { |
| 94 | // Set the Cloudflare optimization cookie from the front end, client-side, |
| 95 | // so HTML responses stay cacheable. |
| 96 | add_action( 'wp_head', array( $this, 'print_cookie_script' ), 0 ); |
| 97 | |
| 98 | // Keep image/font settings in sync when site capabilities change. |
| 99 | add_action( 'set_transient_nfd_site_capabilities', array( $this, 'on_site_capabilities_change' ), 10, 2 ); |
| 100 | |
| 101 | // One-time removal of the obsolete Set-Cookie `.htaccess` block. |
| 102 | add_action( 'init', array( $this, 'maybe_remove_legacy_htaccess_fragment' ) ); |
| 103 | } |
| 104 | |
| 105 | /** |
| 106 | * Callback for when the `nfd_site_capabilities` transient is set. |
| 107 | * |
| 108 | * Triggers a refresh of image and font optimization settings based on updated |
| 109 | * site capabilities. |
| 110 | * |
| 111 | * @since 1.0.0 |
| 112 | * |
| 113 | * @param mixed $value The value being set in the transient. |
| 114 | * @return void |
| 115 | */ |
| 116 | public function on_site_capabilities_change( $value ): void { |
| 117 | if ( is_array( $value ) ) { |
| 118 | ImageSettings::maybe_refresh_with_capabilities( $value ); |
| 119 | FontSettings::maybe_refresh_with_capabilities( $value ); |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | /** |
| 124 | * Compute the deterministic cookie value for the currently-enabled CF features. |
| 125 | * |
| 126 | * Mirrors the historical encoding exactly — concatenated 8-char sha1 prefixes |
| 127 | * for mirage, polish, and fonts in that order — so the value the Cloudflare |
| 128 | * edge rules already key on is unchanged. Returns an empty string when no |
| 129 | * features are enabled. |
| 130 | * |
| 131 | * @since 1.0.0 |
| 132 | * |
| 133 | * @return string |
| 134 | */ |
| 135 | private function get_cookie_value(): string { |
| 136 | $image_settings = get_option( 'nfd_image_optimization', array() ); |
| 137 | $fonts_settings = get_option( 'nfd_fonts_optimization', array() ); |
| 138 | |
| 139 | $images_cloudflare = isset( $image_settings['cloudflare'] ) ? (array) $image_settings['cloudflare'] : array(); |
| 140 | $fonts_cloudflare = isset( $fonts_settings['cloudflare'] ) ? (array) $fonts_settings['cloudflare'] : array(); |
| 141 | |
| 142 | $mirage_enabled = ! empty( $images_cloudflare['mirage']['value'] ); |
| 143 | $polish_enabled = ! empty( $images_cloudflare['polish']['value'] ); |
| 144 | $fonts_enabled = ! empty( $fonts_cloudflare['fonts']['value'] ); |
| 145 | |
| 146 | $mirage_hash = $mirage_enabled ? substr( sha1( 'mirage' ), 0, 8 ) : ''; |
| 147 | $polish_hash = $polish_enabled ? substr( sha1( 'polish' ), 0, 8 ) : ''; |
| 148 | $fonts_hash = $fonts_enabled ? substr( sha1( 'fonts' ), 0, 8 ) : ''; |
| 149 | |
| 150 | return "{$mirage_hash}{$polish_hash}{$fonts_hash}"; |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * Print a tiny inline script that sets the CF optimization cookie client-side. |
| 155 | * |
| 156 | * Hooked early on `wp_head` (front end only) so the cookie is set before the |
| 157 | * parser reaches most sub-resource requests. When no CF features are enabled |
| 158 | * nothing is printed and any existing cookie simply expires. |
| 159 | * |
| 160 | * @since 1.0.0 |
| 161 | * |
| 162 | * @return void |
| 163 | */ |
| 164 | public function print_cookie_script(): void { |
| 165 | $value = $this->get_cookie_value(); |
| 166 | |
| 167 | if ( '' === $value ) { |
| 168 | return; |
| 169 | } |
| 170 | |
| 171 | $script = sprintf( |
| 172 | "(function(){var n=%s,v=%s;if(document.cookie.indexOf(n+'='+v)===-1){document.cookie=n+'='+v+'; path=/; max-age=%d; SameSite=Lax';}})();", |
| 173 | wp_json_encode( self::COOKIE_NAME ), |
| 174 | wp_json_encode( $value ), |
| 175 | self::COOKIE_TTL |
| 176 | ); |
| 177 | |
| 178 | if ( function_exists( 'wp_print_inline_script_tag' ) ) { |
| 179 | wp_print_inline_script_tag( $script ); |
| 180 | return; |
| 181 | } |
| 182 | |
| 183 | echo '<script>' . $script . '</script>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- value is a sha1 hex string encoded via wp_json_encode. |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * Remove the obsolete Set-Cookie `.htaccess` block from sites that still have it. |
| 188 | * |
| 189 | * Runs EXACTLY ONCE per site (guarded by an option flag set up front, so it can |
| 190 | * never run on more than one request — no per-request retry loop). This touches |
| 191 | * ONLY the Cloudflare optimization block: it removes the single state block keyed |
| 192 | * by {@see self::FRAGMENT_ID} and the one `# BEGIN/END {@see self::MARKER}` region |
| 193 | * from the persisted body, leaving the rest of the state byte-for-byte intact. It |
| 194 | * deliberately does NOT call the htaccess unregister API, which would recompose the |
| 195 | * whole body from block entries; everything else any module wrote is preserved. |
| 196 | * |
| 197 | * The persisted state is read directly from its option, which is always available |
| 198 | * on `init` regardless of whether the htaccess Manager has booted — so a single |
| 199 | * pass is authoritative and there is no boot-timing race that would justify |
| 200 | * retrying. |
| 201 | * |
| 202 | * @since 1.0.0 |
| 203 | * |
| 204 | * @return void |
| 205 | */ |
| 206 | public function maybe_remove_legacy_htaccess_fragment(): void { |
| 207 | if ( $this->is_cleanup_done() ) { |
| 208 | return; |
| 209 | } |
| 210 | |
| 211 | // Record completion immediately so this runs once and only once per site, even |
| 212 | // if the state is a no-op (clean site) or in an unexpected shape. The state is |
| 213 | // read directly from its option below, so one pass is authoritative. |
| 214 | $this->set_cleanup_done(); |
| 215 | |
| 216 | // Surgically remove ONLY the CF optimization block from the persisted state. |
| 217 | // apply reuses the persisted body verbatim, so every other block is kept. |
| 218 | if ( $this->strip_legacy_block_from_state() && class_exists( HtaccessApi::class ) ) { |
| 219 | HtaccessApi::queue_apply( 'nfd-cf-opt-legacy-cleanup' ); |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | /** |
| 224 | * Strip the legacy CF optimization block from the htaccess module's saved state. |
| 225 | * |
| 226 | * Removes both a fragment entry keyed by {@see self::FRAGMENT_ID} and the block |
| 227 | * text from the composed body, then persists the change. The body is what the |
| 228 | * htaccess module writes to disk on its next apply, and clearing it also stops the |
| 229 | * block being re-detected as a legacy marker label on subsequent applies. |
| 230 | * |
| 231 | * @since 1.0.0 |
| 232 | * |
| 233 | * @return bool True if the state was modified. |
| 234 | */ |
| 235 | private function strip_legacy_block_from_state(): bool { |
| 236 | $state = $this->get_htaccess_state(); |
| 237 | |
| 238 | if ( ! is_array( $state ) ) { |
| 239 | return false; |
| 240 | } |
| 241 | |
| 242 | $changed = false; |
| 243 | |
| 244 | if ( isset( $state['blocks'] ) && is_array( $state['blocks'] ) && array_key_exists( self::FRAGMENT_ID, $state['blocks'] ) ) { |
| 245 | unset( $state['blocks'][ self::FRAGMENT_ID ] ); |
| 246 | $changed = true; |
| 247 | } |
| 248 | |
| 249 | if ( isset( $state['body'] ) && is_string( $state['body'] ) && false !== strpos( $state['body'], '# BEGIN ' . self::MARKER ) ) { |
| 250 | $clean = $this->remove_marker_block( $state['body'], self::MARKER ); |
| 251 | if ( $clean !== $state['body'] ) { |
| 252 | $state['body'] = $clean; |
| 253 | $state['checksum'] = hash( 'sha256', $clean ); |
| 254 | $changed = true; |
| 255 | } |
| 256 | } |
| 257 | |
| 258 | if ( ! $changed ) { |
| 259 | return false; |
| 260 | } |
| 261 | |
| 262 | $this->update_htaccess_state( $state ); |
| 263 | |
| 264 | return true; |
| 265 | } |
| 266 | |
| 267 | /** |
| 268 | * Remove a `# BEGIN <marker> ... # END <marker>` block from htaccess body text. |
| 269 | * |
| 270 | * Tolerant of leading whitespace and CR/LF variants; collapses the blank lines |
| 271 | * left behind so the remaining body stays well formed. |
| 272 | * |
| 273 | * @since 1.0.0 |
| 274 | * |
| 275 | * @param string $body The htaccess body text. |
| 276 | * @param string $marker The marker label to remove. |
| 277 | * @return string |
| 278 | */ |
| 279 | private function remove_marker_block( string $body, string $marker ): string { |
| 280 | $quoted = preg_quote( $marker, '/' ); |
| 281 | $pattern = '/(?:\r\n|\r|\n)*[ \t]*#[ \t]*BEGIN[ \t]+' . $quoted . '\b.*?#[ \t]*END[ \t]+' . $quoted . '[^\r\n]*/s'; |
| 282 | |
| 283 | $count = 0; |
| 284 | $cleaned = preg_replace( $pattern, '', $body, -1, $count ); |
| 285 | |
| 286 | // Only treat the body as changed when a complete BEGIN..END block was actually |
| 287 | // removed. A partial/malformed marker leaves the body untouched so the caller |
| 288 | // does not write or trigger a rewrite for a no-op. |
| 289 | if ( null === $cleaned || 0 === $count ) { |
| 290 | return $body; |
| 291 | } |
| 292 | |
| 293 | $cleaned = preg_replace( '/(?:\r\n|\r|\n){3,}/', "\n\n", $cleaned ); |
| 294 | |
| 295 | return trim( (string) $cleaned, "\r\n" ); |
| 296 | } |
| 297 | |
| 298 | /** |
| 299 | * Read the htaccess module's persisted state from its option. |
| 300 | * |
| 301 | * @since 1.0.0 |
| 302 | * |
| 303 | * @return array |
| 304 | */ |
| 305 | private function get_htaccess_state(): array { |
| 306 | $state = is_multisite() |
| 307 | ? get_site_option( self::HTACCESS_STATE_OPTION, array() ) |
| 308 | : get_option( self::HTACCESS_STATE_OPTION, array() ); |
| 309 | |
| 310 | return is_array( $state ) ? $state : array(); |
| 311 | } |
| 312 | |
| 313 | /** |
| 314 | * Persist the htaccess module's state back to its option. |
| 315 | * |
| 316 | * @since 1.0.0 |
| 317 | * |
| 318 | * @param array $state State to persist. |
| 319 | * @return void |
| 320 | */ |
| 321 | private function update_htaccess_state( array $state ): void { |
| 322 | if ( is_multisite() ) { |
| 323 | update_site_option( self::HTACCESS_STATE_OPTION, $state ); |
| 324 | return; |
| 325 | } |
| 326 | |
| 327 | update_option( self::HTACCESS_STATE_OPTION, $state ); |
| 328 | } |
| 329 | |
| 330 | /** |
| 331 | * Whether the one-time cleanup has already completed. |
| 332 | * |
| 333 | * @since 1.0.0 |
| 334 | * |
| 335 | * @return bool |
| 336 | */ |
| 337 | private function is_cleanup_done(): bool { |
| 338 | return (bool) ( is_multisite() |
| 339 | ? get_site_option( self::CLEANUP_FLAG ) |
| 340 | : get_option( self::CLEANUP_FLAG ) ); |
| 341 | } |
| 342 | |
| 343 | /** |
| 344 | * Record that the one-time cleanup has completed. |
| 345 | * |
| 346 | * Stored autoloaded so the guard read on every `init` costs no extra query once |
| 347 | * the flag is set. |
| 348 | * |
| 349 | * @since 1.0.0 |
| 350 | * |
| 351 | * @return void |
| 352 | */ |
| 353 | private function set_cleanup_done(): void { |
| 354 | if ( is_multisite() ) { |
| 355 | update_site_option( self::CLEANUP_FLAG, true ); |
| 356 | return; |
| 357 | } |
| 358 | |
| 359 | update_option( self::CLEANUP_FLAG, true, true ); |
| 360 | } |
| 361 | } |