| @@ -30,12 +30,25 @@ | ||
| 30 | 30 | defined( 'ABSPATH' ) || exit; |
| 31 | 31 | |
| 32 | 32 | final class Activity_Log { |
| 33 | 33 | |
| 34 | + /** | |
| 35 | + * Storage key. An OPTION, not a transient — with a persistent object | |
| 36 | + * cache a transient lives in Redis/Memcached, and `Cache::purge_all()` | |
| 37 | + * calls `wp_cache_flush()` before it records the purge. The log was | |
| 38 | + * therefore erased by the very event it exists to record: on any | |
| 39 | + * Redis/Memcached site "Last purge" could never show more than the one | |
| 40 | + * row written after the flush. A history a flush can evaporate is not a | |
| 41 | + * history. Autoload is off — the log is read in admin contexts only. | |
| 42 | + */ | |
| 43 | + public const OPTION_KEY = 'xspeed_activity_log'; | |
| 44 | + | |
| 45 | + /** Legacy transient the log used to live in; drained once on read. */ | |
| 34 | 46 | public const TRANSIENT_KEY = 'xspeed_activity_log'; |
| 35 | - public const TTL = 2592000; // 30 days | |
| 36 | - public const MAX_ENTRIES = 50; | |
| 37 | 47 | |
| 48 | + public const TTL = 2592000; // 30 days — legacy transient only. | |
| 49 | + public const MAX_ENTRIES = 50; | |
| 50 | + | |
| 38 | 51 | public const INFO = 'info'; |
| 39 | 52 | public const WARN = 'warn'; |
| 40 | 53 | public const ERROR = 'error'; |
| 41 | 54 | public const SUCCESS = 'success'; |
| @@ -53,12 +66,26 @@ | ||
| 53 | 66 | ); |
| 54 | 67 | if ( count( $entries ) > self::MAX_ENTRIES ) { |
| 55 | 68 | $entries = array_slice( $entries, 0, self::MAX_ENTRIES ); |
| 56 | 69 | } |
| 57 | - set_transient( self::TRANSIENT_KEY, $entries, self::TTL ); | |
| 70 | + self::store( $entries ); | |
| 58 | 71 | } |
| 59 | 72 | |
| 60 | 73 | /** |
| 74 | + * Persist the log with autoload disabled, so it never joins | |
| 75 | + * `wp_load_alloptions()` on frontend requests. | |
| 76 | + * | |
| 77 | + * @param array<int,array<string,mixed>> $entries Newest-first entries. | |
| 78 | + */ | |
| 79 | + private static function store( array $entries ): void { | |
| 80 | + if ( false === get_option( self::OPTION_KEY, false ) ) { | |
| 81 | + add_option( self::OPTION_KEY, $entries, '', 'no' ); | |
| 82 | + return; | |
| 83 | + } | |
| 84 | + update_option( self::OPTION_KEY, $entries ); | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 61 | 88 | * Newest-first entries (up to MAX_ENTRIES). Defensive shape coercion |
| 62 | 89 | * so a malformed transient never breaks the dashboard. |
| 63 | 90 | * |
| 64 | 91 | * @return array<int,array{ts:int,type:string,message:string,severity:string}> |
| @@ -63,10 +90,23 @@ | ||
| 63 | 90 | * |
| 64 | 91 | * @return array<int,array{ts:int,type:string,message:string,severity:string}> |
| 65 | 92 | */ |
| 66 | 93 | public static function entries(): array { |
| 67 | - $raw = get_transient( self::TRANSIENT_KEY ); | |
| 94 | + $raw = get_option( self::OPTION_KEY, null ); | |
| 95 | + | |
| 96 | + // One-time migration off the old transient. Done lazily on read so | |
| 97 | + // no upgrade routine has to run first, and so history written by a | |
| 98 | + // previous version isn't thrown away. | |
| 68 | 99 | if ( ! is_array( $raw ) ) { |
| 100 | + $legacy = get_transient( self::TRANSIENT_KEY ); | |
| 101 | + if ( is_array( $legacy ) ) { | |
| 102 | + self::store( $legacy ); | |
| 103 | + delete_transient( self::TRANSIENT_KEY ); | |
| 104 | + $raw = $legacy; | |
| 105 | + } | |
| 106 | + } | |
| 107 | + | |
| 108 | + if ( ! is_array( $raw ) ) { | |
| 69 | 109 | return array(); |
| 70 | 110 | } |
| 71 | 111 | $out = array(); |
| 72 | 112 | foreach ( $raw as $e ) { |
| @@ -82,8 +122,9 @@ | ||
| 82 | 122 | return $out; |
| 83 | 123 | } |
| 84 | 124 | |
| 85 | 125 | public static function clear(): void { |
| 126 | + delete_option( self::OPTION_KEY ); | |
| 86 | 127 | delete_transient( self::TRANSIENT_KEY ); |
| 87 | 128 | } |
| 88 | 129 | |
| 89 | 130 | /** |
| @@ -98,10 +139,10 @@ | ||
| 98 | 139 | * Rewrites any `<secret_key> old→new` fragment to `<secret_key> changed`, |
| 99 | 140 | * preserving the rest of the entry so the causal history survives. |
| 100 | 141 | */ |
| 101 | 142 | public static function redact_legacy_secrets(): void { |
| 102 | - $entries = get_transient( self::TRANSIENT_KEY ); | |
| 103 | - if ( ! is_array( $entries ) || empty( $entries ) ) { | |
| 143 | + $entries = self::entries(); | |
| 144 | + if ( empty( $entries ) ) { | |
| 104 | 145 | return; |
| 105 | 146 | } |
| 106 | 147 | |
| 107 | 148 | $changed = false; |
| @@ -126,8 +167,8 @@ | ||
| 126 | 167 | } |
| 127 | 168 | } |
| 128 | 169 | |
| 129 | 170 | if ( $changed ) { |
| 130 | - set_transient( self::TRANSIENT_KEY, $entries, self::TTL ); | |
| 171 | + self::store( $entries ); | |
| 131 | 172 | } |
| 132 | 173 | } |
| 133 | 174 | } |