| @@ -65,8 +65,19 @@ | ||
| 65 | 65 | $this->timeout, |
| 66 | 66 | $flags |
| 67 | 67 | ); |
| 68 | 68 | if ( ! $sock ) { |
| 69 | + // The `@` above hides the warning from output/logs, but a failed | |
| 70 | + // connect (e.g. an unreachable/unresolvable host) still leaves a | |
| 71 | + // PHP warning in `error_get_last()`. WP reads that at | |
| 72 | + // `admin_body_class` time and tags the page `php-error` — which | |
| 73 | + // renders an empty banner above the admin menu even though we | |
| 74 | + // handle the failure gracefully (caller falls back to a | |
| 75 | + // non-persistent cache). Clear it so a degraded-but-handled Redis | |
| 76 | + // backend doesn't masquerade as a site error. | |
| 77 | + if ( function_exists( 'error_clear_last' ) ) { | |
| 78 | + error_clear_last(); | |
| 79 | + } | |
| 69 | 80 | return false; |
| 70 | 81 | } |
| 71 | 82 | stream_set_timeout( $sock, (int) $this->timeout, (int) ( ( $this->timeout - (int) $this->timeout ) * 1000000 ) ); |
| 72 | 83 | $this->sock = $sock; |
| @@ -78,9 +89,23 @@ | ||
| 78 | 89 | } |
| 79 | 90 | |
| 80 | 91 | // --- Commands ----------------------------------------------------------- |
| 81 | 92 | |
| 82 | - public function auth( string $password ) { | |
| 93 | + /** | |
| 94 | + * Authenticate. With a username (Redis 6+ ACL) emit the two-argument | |
| 95 | + * `AUTH <user> <pass>`; without one fall back to the legacy | |
| 96 | + * single-argument `AUTH <pass>` (which authenticates as the built-in | |
| 97 | + * `default` user). Managed hosts that provision a dedicated ACL user | |
| 98 | + * and disable `default` reject the one-arg form with WRONGPASS, so the | |
| 99 | + * username must be threaded through here. (FBS-83118) | |
| 100 | + * | |
| 101 | + * @param string $password Redis password. | |
| 102 | + * @param string $username Optional ACL username; '' = legacy default user. | |
| 103 | + */ | |
| 104 | + public function auth( string $password, string $username = '' ) { | |
| 105 | + if ( '' !== $username ) { | |
| 106 | + return $this->command( array( 'AUTH', $username, $password ) ); | |
| 107 | + } | |
| 83 | 108 | return $this->command( array( 'AUTH', $password ) ); |
| 84 | 109 | } |
| 85 | 110 | |
| 86 | 111 | public function select( int $db ) { |
| @@ -144,8 +169,40 @@ | ||
| 144 | 169 | |
| 145 | 170 | public function flushDB(): bool { |
| 146 | 171 | $r = $this->command( array( 'FLUSHDB' ) ); |
| 147 | 172 | return '+OK' === $r || 'OK' === $r; |
| 173 | + } | |
| 174 | + | |
| 175 | + /** | |
| 176 | + * Delete every key matching a glob-style pattern, using a non-blocking | |
| 177 | + * SCAN cursor (never KEYS, which blocks the whole server on large | |
| 178 | + * datasets). Returns the number of keys deleted. Used for prefix-scoped | |
| 179 | + * flushes so we only ever touch this site's namespace, never the whole | |
| 180 | + * Redis DB. (FBS-83119) | |
| 181 | + * | |
| 182 | + * @param string $pattern e.g. "salt:*" or "salt:*:options:*". | |
| 183 | + * @param int $count SCAN COUNT hint (batch size per round trip). | |
| 184 | + */ | |
| 185 | + public function delete_by_pattern( string $pattern, int $count = 500 ): int { | |
| 186 | + $deleted = 0; | |
| 187 | + $cursor = '0'; | |
| 188 | + do { | |
| 189 | + $reply = $this->command( array( 'SCAN', $cursor, 'MATCH', $pattern, 'COUNT', (string) $count ) ); | |
| 190 | + // Expected: [ next_cursor, [ key, key, ... ] ]. Anything else | |
| 191 | + // (false on socket error, malformed) ends the loop safely. | |
| 192 | + if ( ! is_array( $reply ) || count( $reply ) < 2 || ! is_array( $reply[1] ) ) { | |
| 193 | + break; | |
| 194 | + } | |
| 195 | + $cursor = (string) $reply[0]; | |
| 196 | + $keys = $reply[1]; | |
| 197 | + if ( ! empty( $keys ) ) { | |
| 198 | + // DEL accepts variadic keys — one round trip per batch. | |
| 199 | + $args = array_merge( array( 'DEL' ), array_map( 'strval', $keys ) ); | |
| 200 | + $n = $this->command( $args ); | |
| 201 | + $deleted += is_int( $n ) ? $n : 0; | |
| 202 | + } | |
| 203 | + } while ( '0' !== $cursor ); | |
| 204 | + return $deleted; | |
| 148 | 205 | } |
| 149 | 206 | |
| 150 | 207 | public function close(): void { |
| 151 | 208 | if ( is_resource( $this->sock ) && ! $this->persistent ) { |