| 1 |
<?php |
| 2 |
/** |
| 3 |
* The WP_CACHE define, and how to strip it out of wp-config.php. |
| 4 |
* |
| 5 |
* Deliberately a plain function in its own file rather than a method on |
| 6 |
* Cache: `uninstall.php` runs standalone — WordPress loads it without |
| 7 |
* bootstrapping the plugin, so no class of ours exists there. Keeping the |
| 8 |
* pattern in one requirable file is what stops the two call sites drifting. |
| 9 |
* They already had, which is the bug this fixes: the removal regex was |
| 10 |
* copy-pasted, and hardening one copy would have left the other broken. |
| 11 |
* |
| 12 |
* @package XSpeed |
| 13 |
*/ |
| 14 |
|
| 15 |
defined( 'ABSPATH' ) || exit; |
| 16 |
|
| 17 |
if ( ! function_exists( 'xspeed_has_canonical_dropin_signature' ) ) { |
| 18 |
/** |
| 19 |
* Prove xSpeed ownership from an exact standalone PHP comment marker. |
| 20 |
* |
| 21 |
* A loose substring is unsafe: foreign code or data can mention the token. |
| 22 |
* Tokenization excludes strings, while the anchored pattern excludes prose. |
| 23 |
* |
| 24 |
* @param string $source advanced-cache.php contents. |
| 25 |
* @return bool |
| 26 |
*/ |
| 27 |
function xspeed_has_canonical_dropin_signature( $source ) { |
| 28 |
if ( ! is_string( $source ) || '' === $source || ! function_exists( 'token_get_all' ) ) { |
| 29 |
return false; |
| 30 |
} |
| 31 |
/* |
| 32 |
* ONE channel: an anchored token comment. That is what every xSpeed |
| 33 |
* drop-in ever shipped carries (verified across this file's whole |
| 34 |
* history). |
| 35 |
* |
| 36 |
* There used to be a second channel that accepted a PHP identifier |
| 37 |
* named XSPEED_DROPIN, and it leaked three times: first any mention |
| 38 |
* of the identifier anywhere in the file, then — once narrowed to a |
| 39 |
* `const` declaration — `use const XSPEED_DROPIN` and |
| 40 |
* `class Foo { const XSPEED_DROPIN = 1; }`. Each fix narrowed the |
| 41 |
* shape and the next review found another one, because "a token |
| 42 |
* arrangement only we would write" is not a property you can pin |
| 43 |
* down by enumeration. |
| 44 |
* |
| 45 |
* It is gone rather than narrowed a fourth time. remove_dropin() and |
| 46 |
* uninstall.php DELETE on this verdict, so a false positive destroys |
| 47 |
* another plugin's live page cache; the channel bought nothing our |
| 48 |
* own files ever needed. |
| 49 |
*/ |
| 50 |
$comments = array(); |
| 51 |
foreach ( token_get_all( $source ) as $token ) { |
| 52 |
$id = is_array( $token ) ? $token[0] : null; |
| 53 |
if ( T_OPEN_TAG === $id || T_WHITESPACE === $id ) { |
| 54 |
continue; |
| 55 |
} |
| 56 |
// A BOM, or blank bytes, before `<?php` arrive as inline HTML. An |
| 57 |
// editor or FTP client re-saving our own drop-in that way must not |
| 58 |
// turn it foreign — we would then refuse to update or remove our |
| 59 |
// own file, and uninstall would leave it behind. |
| 60 |
if ( T_INLINE_HTML === $id && '' === trim( $token[1], " \t\r\n\0\x0B\xEF\xBB\xBF" ) ) { |
| 61 |
continue; |
| 62 |
} |
| 63 |
if ( ! in_array( $id, array( T_COMMENT, T_DOC_COMMENT ), true ) ) { |
| 64 |
break; // Real code before any comment: there is no header. |
| 65 |
} |
| 66 |
foreach ( preg_split( '/\R/', $token[1] ) ?: array() as $line ) { |
| 67 |
$line = preg_replace( '/^\s*(?:\/\*+|\*|\/\/|#)\s*/', '', $line ); |
| 68 |
$line = preg_replace( '/\s*\*\/\s*$/', '', (string) $line ); |
| 69 |
if ( '' !== trim( (string) $line ) ) { |
| 70 |
$comments[] = trim( (string) $line ); |
| 71 |
} |
| 72 |
} |
| 73 |
break; |
| 74 |
} |
| 75 |
// The marker must OPEN the header. A competitor that documents rival |
| 76 |
// markers line by line, or carries an interop note, has not handed us |
| 77 |
// its file — and this verdict authorizes deleting their live drop-in. |
| 78 |
$first = isset( $comments[0] ) ? $comments[0] : ''; |
| 79 |
if ( '' !== $first && preg_match( '/^XSPEED_DROPIN(?:\s+(?:v(?:ersion)?\s*)?\d[A-Za-z0-9._-]*)?\s*$/i', $first ) ) { |
| 80 |
return true; |
| 81 |
} |
| 82 |
return false; |
| 83 |
} |
| 84 |
} |
| 85 |
|
| 86 |
if ( ! function_exists( 'xspeed_wp_cache_receipt_matches' ) ) { |
| 87 |
/** |
| 88 |
* Current-source proof that the stored xSpeed receipt still owns the line. |
| 89 |
* |
| 90 |
* @param string $source wp-config.php contents. |
| 91 |
* @param string $receipt Stored ownership receipt. |
| 92 |
* @return bool |
| 93 |
*/ |
| 94 |
function xspeed_wp_cache_receipt_matches( $source, $receipt ) { |
| 95 |
return is_string( $source ) && is_string( $receipt ) |
| 96 |
&& 1 === preg_match( '/^[a-f0-9]{32}$/', $receipt ) |
| 97 |
&& false !== strpos( $source, 'xSpeed owner:' . $receipt ); |
| 98 |
} |
| 99 |
} |
| 100 |
|
| 101 |
if ( ! function_exists( 'xspeed_alternative_block_openers' ) ) { |
| 102 |
/** |
| 103 |
* Token indexes of the `:` that opens an alternative-syntax block. |
| 104 |
* |
| 105 |
* Resolved by walking each control structure's condition to its matching |
| 106 |
* `)` and asking what follows, because the condition can contain anything |
| 107 |
* — a `for` header always contains semicolons, a closure or `match` in an |
| 108 |
* `if` contains braces, a named argument contains its own colon. An |
| 109 |
* earlier version watched for a `:` after an opener keyword and let any |
| 110 |
* `;` or `{` cancel it, which meant `for ( $i = 0; $i < $n; $i++ ):` |
| 111 |
* never registered at all and a define inside it read as top level — |
| 112 |
* licensing a rewrite of somebody's conditional configuration. |
| 113 |
* |
| 114 |
* `else` and `elseif` are deliberately absent: they CONTINUE the block |
| 115 |
* their `if` opened, and counting them left the depth permanently |
| 116 |
* inflated so every later top-level define looked conditional. |
| 117 |
* |
| 118 |
* @param array<int,mixed> $raw Output of token_get_all(). |
| 119 |
* @return array<int,bool> Indexes, as keys. |
| 120 |
*/ |
| 121 |
function xspeed_alternative_block_openers( array $raw ) { |
| 122 |
$trivia = array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ); |
| 123 |
$openers = array( T_IF, T_WHILE, T_FOR, T_FOREACH, T_SWITCH ); |
| 124 |
if ( defined( 'T_DECLARE' ) ) { |
| 125 |
$openers[] = T_DECLARE; |
| 126 |
} |
| 127 |
$next = static function ( $from ) use ( $raw, $trivia ) { |
| 128 |
$count = count( $raw ); |
| 129 |
for ( $i = $from; $i < $count; $i++ ) { |
| 130 |
if ( ! is_array( $raw[ $i ] ) || ! in_array( $raw[ $i ][0], $trivia, true ) ) { |
| 131 |
return $i; |
| 132 |
} |
| 133 |
} |
| 134 |
return null; |
| 135 |
}; |
| 136 |
|
| 137 |
$opens = array(); |
| 138 |
$count = count( $raw ); |
| 139 |
for ( $i = 0; $i < $count; $i++ ) { |
| 140 |
if ( ! is_array( $raw[ $i ] ) || ! in_array( $raw[ $i ][0], $openers, true ) ) { |
| 141 |
continue; |
| 142 |
} |
| 143 |
$open = $next( $i + 1 ); |
| 144 |
if ( null === $open || '(' !== $raw[ $open ] ) { |
| 145 |
continue; |
| 146 |
} |
| 147 |
$depth = 0; |
| 148 |
$close = null; |
| 149 |
for ( $j = $open; $j < $count; $j++ ) { |
| 150 |
if ( '(' === $raw[ $j ] ) { |
| 151 |
++$depth; |
| 152 |
} elseif ( ')' === $raw[ $j ] ) { |
| 153 |
--$depth; |
| 154 |
if ( 0 === $depth ) { |
| 155 |
$close = $j; |
| 156 |
break; |
| 157 |
} |
| 158 |
} |
| 159 |
} |
| 160 |
if ( null === $close ) { |
| 161 |
continue; |
| 162 |
} |
| 163 |
$after = $next( $close + 1 ); |
| 164 |
if ( null !== $after && ':' === $raw[ $after ] ) { |
| 165 |
$opens[ $after ] = true; |
| 166 |
} |
| 167 |
} |
| 168 |
return $opens; |
| 169 |
} |
| 170 |
} |
| 171 |
|
| 172 |
if ( ! function_exists( 'xspeed_parse_wp_cache_defines' ) ) { |
| 173 |
/** |
| 174 |
* Token-aware WP_CACHE parser. |
| 175 |
* |
| 176 |
* Comments and strings are tokens, so text that merely contains a fake |
| 177 |
* define can never become configuration. Offsets are byte offsets into the |
| 178 |
* original source and are used by the rewriter below. |
| 179 |
* |
| 180 |
* @param string $source wp-config.php contents. |
| 181 |
* @return array{state:string,defines:array<int,array{start:int,end:int,value:string}>} |
| 182 |
*/ |
| 183 |
function xspeed_parse_wp_cache_defines( $source ) { |
| 184 |
if ( ! is_string( $source ) || '' === $source || ! function_exists( 'token_get_all' ) ) { |
| 185 |
return array( |
| 186 |
'state' => 'undefined', |
| 187 |
'defines' => array(), |
| 188 |
); |
| 189 |
} |
| 190 |
|
| 191 |
$raw = token_get_all( $source ); |
| 192 |
$tokens = array(); |
| 193 |
$offset = 0; |
| 194 |
$depth = 0; |
| 195 |
$alt = 0; |
| 196 |
$alt_opens = xspeed_alternative_block_openers( $raw ); |
| 197 |
$alt_close = array(); |
| 198 |
foreach ( array( 'T_ENDIF', 'T_ENDWHILE', 'T_ENDFOR', 'T_ENDFOREACH', 'T_ENDSWITCH', 'T_ENDDECLARE' ) as $name ) { |
| 199 |
if ( defined( $name ) ) { |
| 200 |
$alt_close[] = constant( $name ); |
| 201 |
} |
| 202 |
} |
| 203 |
foreach ( $raw as $index => $token ) { |
| 204 |
$id = is_array( $token ) ? $token[0] : null; |
| 205 |
$text = is_array( $token ) ? $token[1] : $token; |
| 206 |
/* |
| 207 |
* Brace depth at the START of each token. A define nested inside |
| 208 |
* anything — a host's `if ( ! defined( 'WP_CACHE' ) ) { … }`, an |
| 209 |
* environment switch — is not a statement we may rewrite: the |
| 210 |
* literal we read is not necessarily what runs, and replacing it |
| 211 |
* edits someone's conditional configuration. |
| 212 |
* |
| 213 |
* Only RAW `{` / `}` count. A `}` inside an interpolated string |
| 214 |
* ("}{$a}", or the heredoc equivalent) arrives as a typed |
| 215 |
* T_ENCAPSED_AND_WHITESPACE token whose text happens to be `}`; |
| 216 |
* counting it desynced the depth and unmasked a define that |
| 217 |
* really was inside someone's block. The interpolation OPENERS |
| 218 |
* are typed (T_CURLY_OPEN / T_DOLLAR_OPEN_CURLY_BRACES) and are |
| 219 |
* closed by a raw `}`, so they are counted to keep the pair |
| 220 |
* balanced. |
| 221 |
*/ |
| 222 |
/* |
| 223 |
* Alternative syntax opens a block with `:` and closes it with an |
| 224 |
* `endif`/`endwhile`/... keyword, no braces at all. Tracking only |
| 225 |
* braces meant a define that was not the FIRST statement of such |
| 226 |
* a block read as top level — the boundary token in front of it |
| 227 |
* is the previous statement's `;` — so an enable rewrote a host's |
| 228 |
* staging-only define in place and a disable cut it out of the |
| 229 |
* block. |
| 230 |
*/ |
| 231 |
if ( in_array( $id, $alt_close, true ) && $alt > 0 ) { |
| 232 |
--$alt; |
| 233 |
} |
| 234 |
// A BOM or stray blank bytes before `<?php` arrive as inline HTML |
| 235 |
// and must not be mistaken for code. |
| 236 |
if ( T_INLINE_HTML === $id && '' === trim( $text, " \t\r\n\0\x0B\xEF\xBB\xBF" ) ) { |
| 237 |
$offset += strlen( $text ); |
| 238 |
continue; |
| 239 |
} |
| 240 |
$raw_punct = ( null === $id ); |
| 241 |
if ( $raw_punct && '}' === $text && $depth > 0 ) { |
| 242 |
--$depth; |
| 243 |
} |
| 244 |
$tokens[] = array( |
| 245 |
'id' => $id, |
| 246 |
'text' => $text, |
| 247 |
'start' => $offset, |
| 248 |
'end' => $offset + strlen( $text ), |
| 249 |
'depth' => $depth + $alt, |
| 250 |
); |
| 251 |
if ( ( $raw_punct && '{' === $text ) |
| 252 |
|| ( defined( 'T_CURLY_OPEN' ) && T_CURLY_OPEN === $id ) |
| 253 |
|| ( defined( 'T_DOLLAR_OPEN_CURLY_BRACES' ) && T_DOLLAR_OPEN_CURLY_BRACES === $id ) ) { |
| 254 |
++$depth; |
| 255 |
} elseif ( isset( $alt_opens[ $index ] ) ) { |
| 256 |
++$alt; |
| 257 |
} |
| 258 |
$offset += strlen( $text ); |
| 259 |
} |
| 260 |
|
| 261 |
$is_trivia = static function ( $token ) { |
| 262 |
return in_array( $token['id'], array( T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ), true ); |
| 263 |
}; |
| 264 |
$next = static function ( $index ) use ( $tokens, $is_trivia ) { |
| 265 |
for ( $index++; isset( $tokens[ $index ] ); $index++ ) { |
| 266 |
if ( ! $is_trivia( $tokens[ $index ] ) ) { |
| 267 |
return $index; |
| 268 |
} |
| 269 |
} |
| 270 |
return null; |
| 271 |
}; |
| 272 |
$previous = static function ( $index ) use ( $tokens, $is_trivia ) { |
| 273 |
for ( $index--; 0 <= $index; $index-- ) { |
| 274 |
if ( ! $is_trivia( $tokens[ $index ] ) ) { |
| 275 |
return $index; |
| 276 |
} |
| 277 |
} |
| 278 |
return null; |
| 279 |
}; |
| 280 |
|
| 281 |
$defines = array(); |
| 282 |
$count = count( $tokens ); |
| 283 |
for ( $i = 0; $i < $count; $i++ ) { |
| 284 |
/* |
| 285 |
* `define` and `\define` are the same call, and wp-config.php files |
| 286 |
* in the wild are written both ways. PHP 8 tokenises the qualified |
| 287 |
* form as ONE T_NAME_FULLY_QUALIFIED token (`\define`), so matching |
| 288 |
* T_STRING alone made the define invisible: the detector then read |
| 289 |
* a site with WP_CACHE on as unclaimed and let an acquisition |
| 290 |
* through, and the rewriter inserted a SECOND define instead of |
| 291 |
* replacing the one that was there. |
| 292 |
* |
| 293 |
* PHP 7.4 splits it into T_NS_SEPARATOR + T_STRING, which did |
| 294 |
* match — so the behaviour differed by PHP version, and there the |
| 295 |
* recorded start missed the leading `\`, leaving a bare backslash |
| 296 |
* in wp-config.php after a strip. `$start` covers the separator |
| 297 |
* for that reason. |
| 298 |
*/ |
| 299 |
$start = $tokens[ $i ]['start']; |
| 300 |
if ( defined( 'T_NAME_FULLY_QUALIFIED' ) && T_NAME_FULLY_QUALIFIED === $tokens[ $i ]['id'] ) { |
| 301 |
if ( '\\define' !== strtolower( $tokens[ $i ]['text'] ) ) { |
| 302 |
continue; |
| 303 |
} |
| 304 |
} elseif ( T_STRING === $tokens[ $i ]['id'] && 'define' === strtolower( $tokens[ $i ]['text'] ) ) { |
| 305 |
$prior = $previous( $i ); |
| 306 |
if ( null !== $prior && in_array( $tokens[ $prior ]['id'], array( T_OBJECT_OPERATOR, T_DOUBLE_COLON ), true ) ) { |
| 307 |
continue; |
| 308 |
} |
| 309 |
if ( null !== $prior && T_NS_SEPARATOR === $tokens[ $prior ]['id'] ) { |
| 310 |
// PHP 7.4 splits `\define` into T_NS_SEPARATOR + T_STRING. |
| 311 |
// `Foo\define` splits the same way, so check what sits in |
| 312 |
// front of the separator: a name there means someone |
| 313 |
// else's function, not core's, and stepping $start back |
| 314 |
// over the separator would leave `Foo` dangling. |
| 315 |
$before = $previous( $prior ); |
| 316 |
if ( null !== $before && in_array( $tokens[ $before ]['id'], array( T_STRING, T_NS_SEPARATOR ), true ) ) { |
| 317 |
continue; |
| 318 |
} |
| 319 |
$start = $tokens[ $prior ]['start']; |
| 320 |
} |
| 321 |
} else { |
| 322 |
continue; |
| 323 |
} |
| 324 |
$open = $next( $i ); |
| 325 |
$name = null !== $open ? $next( $open ) : null; |
| 326 |
if ( null === $name || '(' !== $tokens[ $open ]['text'] || T_CONSTANT_ENCAPSED_STRING !== $tokens[ $name ]['id'] ) { |
| 327 |
continue; |
| 328 |
} |
| 329 |
$constant = trim( $tokens[ $name ]['text'], "'\"" ); |
| 330 |
$comma = $next( $name ); |
| 331 |
if ( 'WP_CACHE' !== strtoupper( $constant ) || null === $comma || ',' !== $tokens[ $comma ]['text'] ) { |
| 332 |
continue; |
| 333 |
} |
| 334 |
|
| 335 |
$depth = 1; |
| 336 |
$close = null; |
| 337 |
for ( $j = $comma + 1; $j < $count; $j++ ) { |
| 338 |
if ( '(' === $tokens[ $j ]['text'] ) { |
| 339 |
++$depth; |
| 340 |
} elseif ( ')' === $tokens[ $j ]['text'] && 0 === --$depth ) { |
| 341 |
$close = $j; |
| 342 |
break; |
| 343 |
} |
| 344 |
} |
| 345 |
if ( null === $close ) { |
| 346 |
continue; |
| 347 |
} |
| 348 |
$end_token = $next( $close ); |
| 349 |
$end = ( null !== $end_token && ';' === $tokens[ $end_token ]['text'] ) ? $tokens[ $end_token ]['end'] : $tokens[ $close ]['end']; |
| 350 |
$value = ''; |
| 351 |
for ( $j = $comma + 1; $j < $close; $j++ ) { |
| 352 |
if ( ! $is_trivia( $tokens[ $j ] ) ) { |
| 353 |
$value .= $tokens[ $j ]['text']; |
| 354 |
} |
| 355 |
} |
| 356 |
/* |
| 357 |
* Is this define a statement of its own, or the body of something? |
| 358 |
* |
| 359 |
* Brace depth alone missed the braceless forms, which are the |
| 360 |
* common ones in a wp-config.php: |
| 361 |
* |
| 362 |
* if ( ! defined( 'WP_CACHE' ) ) define( 'WP_CACHE', true ); |
| 363 |
* if ( $host === 'staging' ) |
| 364 |
* define( 'WP_CACHE', false ); |
| 365 |
* |
| 366 |
* Both read as top level, so an enable rewrote a host's |
| 367 |
* conditional in place and a disable cut the statement out — |
| 368 |
* leaving a dangling `if` that silently captures whatever |
| 369 |
* statement follows it. A define whose `DB_NAME` neighbour became |
| 370 |
* conditional is a far worse outcome than a refusal. |
| 371 |
* |
| 372 |
* A real statement can only follow `<?php`, `;`, `{`, `}`, or |
| 373 |
* nothing at all. Anything else — `)` closing a control |
| 374 |
* condition, `else`, `do`, the `:` of alternative syntax — means |
| 375 |
* we are the controlled statement. That closes the `if ( … ): |
| 376 |
* … endif;` gap too. |
| 377 |
*/ |
| 378 |
$before_statement = $previous( $i ); |
| 379 |
if ( null !== $before_statement && $start !== $tokens[ $i ]['start'] ) { |
| 380 |
// The qualified form: step back past the separator we adopted. |
| 381 |
$before_statement = $previous( $before_statement ); |
| 382 |
} |
| 383 |
$boundary = null === $before_statement |
| 384 |
|| T_OPEN_TAG === $tokens[ $before_statement ]['id'] |
| 385 |
|| in_array( $tokens[ $before_statement ]['text'], array( ';', '{', '}' ), true ); |
| 386 |
|
| 387 |
$defines[] = array( |
| 388 |
'start' => $start, |
| 389 |
'end' => $end, |
| 390 |
'value' => $value, |
| 391 |
'conditional' => $tokens[ $i ]['depth'] > 0 || ! $boundary, |
| 392 |
); |
| 393 |
$i = $close; |
| 394 |
} |
| 395 |
|
| 396 |
if ( 0 === count( $defines ) ) { |
| 397 |
$state = 'undefined'; |
| 398 |
} elseif ( 1 < count( $defines ) ) { |
| 399 |
$state = 'duplicate'; |
| 400 |
} elseif ( ! empty( $defines[0]['conditional'] ) ) { |
| 401 |
$state = 'conditional'; |
| 402 |
} else { |
| 403 |
$value = strtolower( trim( $defines[0]['value'] ) ); |
| 404 |
if ( preg_match( '/^(?:true|1|[\'\"]1[\'\"])$/i', $value ) ) { |
| 405 |
$state = 'true'; |
| 406 |
} elseif ( preg_match( '/^(?:false|0|null|[\'\"](?:0)?[\'\"])$/i', $value ) ) { |
| 407 |
$state = 'false'; |
| 408 |
} else { |
| 409 |
$state = 'dynamic'; |
| 410 |
} |
| 411 |
} |
| 412 |
return array( |
| 413 |
'state' => $state, |
| 414 |
'defines' => $defines, |
| 415 |
); |
| 416 |
} |
| 417 |
} |
| 418 |
|
| 419 |
if ( ! function_exists( 'xspeed_drop_receipt_comment' ) ) { |
| 420 |
/** |
| 421 |
* Remove the `// xSpeed owner:<hex>` receipt that trails a WP_CACHE |
| 422 |
* define, taking the whole comment with it. |
| 423 |
* |
| 424 |
* The parser's offsets stop at the `;`, so the receipt we wrote sits |
| 425 |
* outside every rewrite and every removal. Consuming only as far as the |
| 426 |
* hex left anything a user had appended to OUR comment standing as bare |
| 427 |
* code: |
| 428 |
* |
| 429 |
* define( 'WP_CACHE', true ); // xSpeed owner:abc123 do not remove |
| 430 |
* |
| 431 |
* became `do not remove` at top level — a parse error in wp-config.php, |
| 432 |
* so the site white-screens. A `//` comment runs to end of line by |
| 433 |
* definition, so consuming to end of line removes exactly the comment |
| 434 |
* token and nothing else. It also clears the stacked receipts left by |
| 435 |
* the historic append bug, which all share that one line. |
| 436 |
* |
| 437 |
* @param string $tail Source immediately after the define's `;`. |
| 438 |
* @return string |
| 439 |
*/ |
| 440 |
function xspeed_drop_receipt_comment( $tail ) { |
| 441 |
if ( ! is_string( $tail ) ) { |
| 442 |
return $tail; |
| 443 |
} |
| 444 |
return (string) preg_replace( '#^[ \t]*//[ \t]*xSpeed owner:[a-f0-9]*[^\r\n]*#i', '', $tail, 1 ); |
| 445 |
} |
| 446 |
} |
| 447 |
|
| 448 |
if ( ! function_exists( 'xspeed_rewrite_wp_cache_define' ) ) { |
| 449 |
/** |
| 450 |
* Return rewritten source, or null when the source is ambiguous. |
| 451 |
* |
| 452 |
* @param string $source wp-config.php contents. |
| 453 |
* @param bool $enable Desired literal state. |
| 454 |
* @param string $marker Optional xSpeed ownership receipt. |
| 455 |
* @return string|null |
| 456 |
*/ |
| 457 |
function xspeed_rewrite_wp_cache_define( $source, $enable, $marker = '' ) { |
| 458 |
$parsed = xspeed_parse_wp_cache_defines( $source ); |
| 459 |
if ( in_array( $parsed['state'], array( 'duplicate', 'dynamic', 'conditional' ), true ) ) { |
| 460 |
return null; |
| 461 |
} |
| 462 |
$suffix = '' !== $marker ? ' // xSpeed owner:' . preg_replace( '/[^a-f0-9]/i', '', $marker ) : ''; |
| 463 |
$replacement = $enable ? "define( 'WP_CACHE', true );" . $suffix : ''; |
| 464 |
if ( ! empty( $parsed['defines'] ) ) { |
| 465 |
$define = $parsed['defines'][0]; |
| 466 |
/* |
| 467 |
* The parser's `end` is the `;` of the define, so the ownership |
| 468 |
* receipt WE wrote last time sits just past it and is not part of |
| 469 |
* what gets replaced. Rewriting therefore used to append a second |
| 470 |
* receipt to the same line, and a third, and a fourth: the enable |
| 471 |
* transaction runs on every admin_init, so the line grew by 26 |
| 472 |
* bytes per wp-admin request. Disabling had the mirror bug — the |
| 473 |
* define went, the receipt comment stayed behind forever. |
| 474 |
* |
| 475 |
* Consume it here so a rewrite is idempotent and a removal is |
| 476 |
* complete. Only our own comment, only when it directly follows |
| 477 |
* the statement. |
| 478 |
*/ |
| 479 |
$tail = substr( $source, $define['end'] ); |
| 480 |
$tail = xspeed_drop_receipt_comment( $tail ); |
| 481 |
return substr( $source, 0, $define['start'] ) . $replacement . $tail; |
| 482 |
} |
| 483 |
if ( ! $enable ) { |
| 484 |
return $source; |
| 485 |
} |
| 486 |
$position = strpos( $source, '<?php' ); |
| 487 |
if ( false === $position ) { |
| 488 |
return null; |
| 489 |
} |
| 490 |
$position += 5; |
| 491 |
return substr( $source, 0, $position ) . "\n" . $replacement . substr( $source, $position ); |
| 492 |
} |
| 493 |
} |
| 494 |
|
| 495 |
if ( ! function_exists( 'xspeed_strip_wp_cache_define' ) ) { |
| 496 |
/** |
| 497 |
* Remove every `define( 'WP_CACHE', … );` statement from wp-config source. |
| 498 |
* |
| 499 |
* The old pattern hardcoded a lowercase `true` with no `/i`, so |
| 500 |
* `TRUE`, `True`, `1`, `'1'` and `"1"` never matched — disabling the |
| 501 |
* cache (or uninstalling) silently left the constant behind. Those |
| 502 |
* spellings are common: several hosts' one-click stacks and older |
| 503 |
* tutorials write `TRUE` or `1`, and any plugin that previously owned |
| 504 |
* the constant may have written it in its own style. The orphan then |
| 505 |
* confuses the next caching plugin the site installs — W3 Total Cache |
| 506 |
* and WP Rocket both branch on it — and makes a "clean uninstall" not |
| 507 |
* clean. (#9) |
| 508 |
* |
| 509 |
* Uses the token parser above, so comments and string contents are ignored |
| 510 |
* while any real spelling is removed: |
| 511 |
* true / TRUE / 1 / '1' / false / 0, with or without inner spaces. |
| 512 |
* `false` is removed too — leaving `define( 'WP_CACHE', false );` |
| 513 |
* behind is the same orphan problem, just quieter. |
| 514 |
* |
| 515 |
* Only our own statement shape is targeted. A commented-out line keeps |
| 516 |
* its `#`/`//` prefix and is left alone by the leading-boundary match; |
| 517 |
* a define built dynamically (concatenation, a variable) is not a |
| 518 |
* literal statement and is out of scope for a regex — those are |
| 519 |
* vanishingly rare in wp-config.php and unsafe to rewrite blind. |
| 520 |
* |
| 521 |
* @param string $config Raw wp-config.php contents. |
| 522 |
* @return string Contents with the define(s) removed. |
| 523 |
*/ |
| 524 |
function xspeed_strip_wp_cache_define( $config ) { |
| 525 |
if ( ! is_string( $config ) || '' === $config ) { |
| 526 |
return $config; |
| 527 |
} |
| 528 |
|
| 529 |
$parsed = xspeed_parse_wp_cache_defines( $config ); |
| 530 |
foreach ( array_reverse( $parsed['defines'] ) as $define ) { |
| 531 |
// A define inside someone's conditional is theirs, and cutting the |
| 532 |
// statement out of the block is how a `{ … }` loses its body. |
| 533 |
if ( ! empty( $define['conditional'] ) ) { |
| 534 |
continue; |
| 535 |
} |
| 536 |
// Take our ownership receipt comment with the statement it |
| 537 |
// annotates; leaving `// xSpeed owner:…` in an uninstalled site's |
| 538 |
// wp-config.php is the same orphan this function exists to stop. |
| 539 |
$tail = xspeed_drop_receipt_comment( substr( $config, $define['end'] ) ); |
| 540 |
$config = substr( $config, 0, $define['start'] ) . $tail; |
| 541 |
} |
| 542 |
return $config; |
| 543 |
} |
| 544 |
} |
| 545 |
|