| @@ -8,10 +8,11 @@ | ||
| 8 | 8 | */ |
| 9 | 9 | |
| 10 | 10 | namespace RadiusTheme\SB\Helpers; |
| 11 | 11 | |
| 12 | +use W3TC\Dispatcher; | |
| 12 | 13 | use RadiusTheme\SB\Traits\SingletonTrait; |
| 13 | -use W3TC\Dispatcher; | |
| 14 | +use RadiusTheme\SB\Controllers\AssetRegistry; | |
| 14 | 15 | |
| 15 | 16 | // Do not allow directly accessing this file. |
| 16 | 17 | if ( ! defined( 'ABSPATH' ) ) { |
| 17 | 18 | exit( 'This script cannot be accessed directly.' ); |
| @@ -22,17 +23,330 @@ | ||
| 22 | 23 | */ |
| 23 | 24 | class Cache { |
| 24 | 25 | |
| 25 | 26 | /** |
| 27 | + * Option key holding the list of transients owned by the plugin. | |
| 28 | + * | |
| 29 | + * @var string | |
| 30 | + */ | |
| 31 | + const TRANSIENT_INDEX_KEY = 'shopbuilder_transient_cached_data'; | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * Lifetime of the transient index. | |
| 35 | + * | |
| 36 | + * Must outlive the individual transients it tracks (12 hours) so a purge can | |
| 37 | + * still find them, while keeping the option out of the autoloaded set. | |
| 38 | + * | |
| 39 | + * @var int | |
| 40 | + */ | |
| 41 | + const TRANSIENT_INDEX_TTL = WEEK_IN_SECONDS; | |
| 42 | + | |
| 43 | + /** | |
| 44 | + * Transient key prefixes owned by ShopBuilder. | |
| 45 | + * | |
| 46 | + * A purge resolves these against wp_options directly instead of consulting a | |
| 47 | + * stored list of keys. The stored list had to be read, appended to, deduped | |
| 48 | + * and rewritten on every frontend cache miss, which is O(n) per write and so | |
| 49 | + * O(n^2) to rebuild after an update wipes it. A prefix lookup moves that cost | |
| 50 | + * off the frontend entirely and onto the purge, which runs a few times a day. | |
| 51 | + * | |
| 52 | + * The template_for_* families are listed individually rather than collapsed | |
| 53 | + * into a single 'template_for_' prefix: that stem carries no plugin | |
| 54 | + * namespace, so a broad match could delete another plugin's transients. | |
| 55 | + * | |
| 56 | + * @var string[] | |
| 57 | + */ | |
| 58 | + const TRANSIENT_PREFIXES = [ | |
| 59 | + 'rtsb_', | |
| 60 | + 'template_for_product_page_', | |
| 61 | + 'template_for_archive_page_', | |
| 62 | + 'template_for_tag_archive_page_', | |
| 63 | + 'template_for_brand_archive_page_', | |
| 64 | + ]; | |
| 65 | + | |
| 66 | + /** | |
| 67 | + * Option holding the cache group rotation state. | |
| 68 | + * | |
| 69 | + * A single combined option rather than separate keys, so the signature, the | |
| 70 | + * group it produced and the generation counter can never drift apart: they | |
| 71 | + * are always written together in one atomic update_option() call. | |
| 72 | + * | |
| 73 | + * Shape: [ 'sig' => '<free>|<pro>', 'group' => '<group name>', 'gen' => int ] | |
| 74 | + * | |
| 75 | + * Autoloaded on purpose - it is a three key array, and once group() becomes | |
| 76 | + * version derived it is read on every request. | |
| 77 | + * | |
| 78 | + * @var string | |
| 79 | + */ | |
| 80 | + const STATE_OPTION = 'rtsb_cache_state'; | |
| 81 | + | |
| 82 | + /** | |
| 83 | + * Group name used before the group became version derived. | |
| 84 | + * | |
| 85 | + * Also the prefix every derived name is built from, so a derived name can | |
| 86 | + * never collide with it: derived names always carry a `_<hash>` suffix. | |
| 87 | + * | |
| 88 | + * @var string | |
| 89 | + */ | |
| 90 | + const LEGACY_GROUP = 'shopbuilder'; | |
| 91 | + | |
| 92 | + /** | |
| 93 | + * Resolved group for this request. | |
| 94 | + * | |
| 95 | + * A property rather than a function local static so maybe_rotate_group() can | |
| 96 | + * clear it after rotating, and the remainder of the request then uses the | |
| 97 | + * group we moved to instead of the one primed before rotation ran. | |
| 98 | + * | |
| 99 | + * @var string|null | |
| 100 | + */ | |
| 101 | + private static $group_memo = null; | |
| 102 | + | |
| 103 | + /** | |
| 104 | + * Whether a full purge already ran during this request. | |
| 105 | + * | |
| 106 | + * Both the free and the pro plugin hook `upgrader_process_complete`, and a | |
| 107 | + * bulk update can match both at once. Without this guard a single update | |
| 108 | + * would run the whole purge (including every third-party cache plugin) more | |
| 109 | + * than once in the same request. | |
| 110 | + * | |
| 111 | + * @var bool | |
| 112 | + */ | |
| 113 | + private static $all_cache_cleared = false; | |
| 114 | + | |
| 115 | + /** | |
| 116 | + * Object cache group owned by the plugin. | |
| 117 | + * | |
| 118 | + * Single source of truth for every wp_cache_* call in both the free and the | |
| 119 | + * pro plugin, so the group can be changed in one place instead of at ninety | |
| 120 | + * separate call sites. | |
| 121 | + * | |
| 122 | + * @return string | |
| 123 | + */ | |
| 124 | + public static function group() { | |
| 125 | + if ( null !== self::$group_memo ) { | |
| 126 | + return self::$group_memo; | |
| 127 | + } | |
| 128 | + | |
| 129 | + if ( self::is_legacy_group_pinned() ) { | |
| 130 | + self::$group_memo = self::LEGACY_GROUP; | |
| 131 | + | |
| 132 | + return self::$group_memo; | |
| 133 | + } | |
| 134 | + | |
| 135 | + $state = self::get_state(); | |
| 136 | + | |
| 137 | + // The stored group is canonical. It is only derived here when no state | |
| 138 | + // exists yet, which means a fresh install or a wiped option. | |
| 139 | + self::$group_memo = '' !== $state['group'] | |
| 140 | + ? $state['group'] | |
| 141 | + : self::compute_group( self::state_signature(), $state['gen'] ); | |
| 142 | + | |
| 143 | + return self::$group_memo; | |
| 144 | + } | |
| 145 | + | |
| 146 | + /** | |
| 147 | + * Whether the site has pinned the group back to the pre-derivation name. | |
| 148 | + * | |
| 149 | + * Escape hatch for sites that need the old behaviour restored without a | |
| 150 | + * downgrade. It suspends the derived naming only. Signature tracking and the | |
| 151 | + * generation counter keep running underneath, so removing the hatch moves the | |
| 152 | + * site onto a group name that has never been used before. | |
| 153 | + * | |
| 154 | + * @return bool | |
| 155 | + */ | |
| 156 | + private static function is_legacy_group_pinned() { | |
| 157 | + return defined( 'RTSB_LEGACY_CACHE_GROUP' ) && RTSB_LEGACY_CACHE_GROUP; | |
| 158 | + } | |
| 159 | + | |
| 160 | + /** | |
| 161 | + * Derive a group name from a signature and generation. | |
| 162 | + * | |
| 163 | + * Deterministic on purpose: two requests racing the same rotation compute the | |
| 164 | + * same name and converge, rather than each inventing a group of its own. | |
| 165 | + * | |
| 166 | + * @param string $signature Version signature. | |
| 167 | + * @param int $generation Monotonic counter. | |
| 168 | + * | |
| 169 | + * @return string | |
| 170 | + */ | |
| 171 | + private static function compute_group( $signature, $generation ) { | |
| 172 | + return self::LEGACY_GROUP . '_' . substr( md5( $signature . '|' . absint( $generation ) ), 0, 8 ); | |
| 173 | + } | |
| 174 | + | |
| 175 | + /** | |
| 176 | + * Signature of the currently installed plugin versions. | |
| 177 | + * | |
| 178 | + * Reads the constants directly rather than rtsb()->has_pro(), which depends | |
| 179 | + * on function_exists( 'rtsbpro' ) and is therefore load order sensitive. Both | |
| 180 | + * constants are defined while the plugin files are being included, so this is | |
| 181 | + * stable from the first hook onwards. | |
| 182 | + * | |
| 183 | + * @return string | |
| 184 | + */ | |
| 185 | + public static function state_signature() { | |
| 186 | + return RTSB_VERSION . '|' . ( defined( 'RTSBPRO_VERSION' ) ? RTSBPRO_VERSION : '' ); | |
| 187 | + } | |
| 188 | + | |
| 189 | + /** | |
| 190 | + * Read the rotation state, normalised against missing or malformed data. | |
| 191 | + * | |
| 192 | + * Never throws and never returns a partial shape, so a hand edited or | |
| 193 | + * truncated option cannot break a request. | |
| 194 | + * | |
| 195 | + * @return array | |
| 196 | + */ | |
| 197 | + public static function get_state() { | |
| 198 | + $state = get_option( self::STATE_OPTION, [] ); | |
| 199 | + | |
| 200 | + if ( ! is_array( $state ) ) { | |
| 201 | + $state = []; | |
| 202 | + } | |
| 203 | + | |
| 204 | + return [ | |
| 205 | + 'sig' => ( isset( $state['sig'] ) && is_string( $state['sig'] ) ) ? $state['sig'] : '', | |
| 206 | + 'group' => ( isset( $state['group'] ) && is_string( $state['group'] ) && '' !== $state['group'] ) ? $state['group'] : '', | |
| 207 | + 'gen' => isset( $state['gen'] ) ? absint( $state['gen'] ) : 0, | |
| 208 | + ]; | |
| 209 | + } | |
| 210 | + | |
| 211 | + /** | |
| 212 | + * Rotate the cache group when the installed version pair changes. | |
| 213 | + * | |
| 214 | + * Rotation is keyed on the version signature only, never on the group name. | |
| 215 | + * Once group() becomes version derived, the generation counter feeds into the | |
| 216 | + * group name, so comparing against the group name instead would change the | |
| 217 | + * name on every rotation and loop forever. | |
| 218 | + * | |
| 219 | + * The generation counter increments on ANY signature change, in either | |
| 220 | + * direction. That is what makes a downgrade safe: reverting to an older | |
| 221 | + * version produces a third, previously unused group rather than reusing the | |
| 222 | + * one that version had before, which could still hold stale entries. | |
| 223 | + * | |
| 224 | + * @return void | |
| 225 | + */ | |
| 226 | + public static function maybe_rotate_group() { | |
| 227 | + $state = self::get_state(); | |
| 228 | + $signature = self::state_signature(); | |
| 229 | + | |
| 230 | + // First run, or the option was wiped or malformed. Record the current | |
| 231 | + // state without touching the cache: there is no known previous group to | |
| 232 | + // reclaim, and incrementing here would burn a generation for nothing. | |
| 233 | + if ( '' === $state['sig'] || '' === $state['group'] ) { | |
| 234 | + self::store_state( $signature, self::group(), $state['gen'] ); | |
| 235 | + | |
| 236 | + return; | |
| 237 | + } | |
| 238 | + | |
| 239 | + $pinned = self::is_legacy_group_pinned(); | |
| 240 | + $signature_changed = ( $signature !== $state['sig'] ); | |
| 241 | + | |
| 242 | + /* | |
| 243 | + * Invariant: the stored group must equal what the stored signature and | |
| 244 | + * generation produce. A violation means the naming scheme changed under | |
| 245 | + * us, which is exactly how the legacy group migrates to a derived one | |
| 246 | + * without requiring a version bump. It also self heals a hand edited or | |
| 247 | + * corrupted generation, in a single rotation. | |
| 248 | + * | |
| 249 | + * Suspended while pinned: a pinned group deliberately does not follow the | |
| 250 | + * derivation, so the check would report a violation on every request and | |
| 251 | + * rotate endlessly. | |
| 252 | + */ | |
| 253 | + $invariant_broken = ( ! $pinned && self::compute_group( $state['sig'], $state['gen'] ) !== $state['group'] ); | |
| 254 | + | |
| 255 | + if ( ! $signature_changed && ! $invariant_broken ) { | |
| 256 | + return; | |
| 257 | + } | |
| 258 | + | |
| 259 | + $previous = $state['group']; | |
| 260 | + $generation = $state['gen'] + 1; | |
| 261 | + $next = $pinned ? self::LEGACY_GROUP : self::compute_group( $signature, $generation ); | |
| 262 | + | |
| 263 | + self::store_state( $signature, $next, $generation ); | |
| 264 | + | |
| 265 | + // Drop any value primed before this ran so the rest of the request uses | |
| 266 | + // the group we just moved to. | |
| 267 | + self::$group_memo = null; | |
| 268 | + | |
| 269 | + // Reclaim only the group we left. Never the one now in use, which is the | |
| 270 | + // case whenever the name has not actually changed. | |
| 271 | + if ( $next !== $previous ) { | |
| 272 | + self::flush_group_if_supported( $previous ); | |
| 273 | + } | |
| 274 | + } | |
| 275 | + | |
| 276 | + /** | |
| 277 | + * Persist the rotation state. | |
| 278 | + * | |
| 279 | + * @param string $signature Version signature. | |
| 280 | + * @param string $group Group name in use for that signature. | |
| 281 | + * @param int $generation Monotonic counter, never reused. | |
| 282 | + * | |
| 283 | + * @return void | |
| 284 | + */ | |
| 285 | + private static function store_state( $signature, $group, $generation ) { | |
| 286 | + update_option( | |
| 287 | + self::STATE_OPTION, | |
| 288 | + [ | |
| 289 | + 'sig' => (string) $signature, | |
| 290 | + 'group' => (string) $group, | |
| 291 | + 'gen' => absint( $generation ), | |
| 292 | + ], | |
| 293 | + true | |
| 294 | + ); | |
| 295 | + } | |
| 296 | + | |
| 297 | + /** | |
| 298 | + * Flush a single object cache group, only where the drop-in supports it. | |
| 299 | + * | |
| 300 | + * Deliberately has no fallback. wp_cache_flush() would empty every group on | |
| 301 | + * the site, including WooCommerce sessions and every other plugin's data, | |
| 302 | + * which is the exact behaviour this work exists to remove. When group | |
| 303 | + * flushing is unavailable the abandoned entries are simply left to expire. | |
| 304 | + * | |
| 305 | + * @param string $group Group to flush. | |
| 306 | + * | |
| 307 | + * @return bool Whether the flush was performed. | |
| 308 | + */ | |
| 309 | + public static function flush_group_if_supported( $group ) { | |
| 310 | + if ( ! is_string( $group ) || '' === $group ) { | |
| 311 | + return false; | |
| 312 | + } | |
| 313 | + | |
| 314 | + if ( ! function_exists( 'wp_cache_supports' ) || ! function_exists( 'wp_cache_flush_group' ) ) { | |
| 315 | + return false; | |
| 316 | + } | |
| 317 | + | |
| 318 | + if ( ! wp_cache_supports( 'flush_group' ) ) { | |
| 319 | + return false; | |
| 320 | + } | |
| 321 | + | |
| 322 | + wp_cache_flush_group( $group ); | |
| 323 | + | |
| 324 | + return true; | |
| 325 | + } | |
| 326 | + | |
| 327 | + /** | |
| 26 | 328 | * Clear the template cache. |
| 27 | 329 | * |
| 28 | 330 | * @since 4.3.0 |
| 29 | 331 | */ |
| 30 | 332 | public static function clear_all_cache() { |
| 333 | + if ( self::$all_cache_cleared ) { | |
| 334 | + return; | |
| 335 | + } | |
| 336 | + | |
| 337 | + self::$all_cache_cleared = true; | |
| 338 | + | |
| 31 | 339 | self::clear_data_cache(); |
| 32 | 340 | self::clear_template_cache(); |
| 341 | + self::clear_transient_cache(); | |
| 342 | + self::clear_asset_cache(); | |
| 343 | + | |
| 344 | + // Purged last on purpose. This is what empties the external page caches, | |
| 345 | + // so it must not run until ShopBuilder's own transients and asset bundles | |
| 346 | + // have been rebuilt - otherwise live traffic arrives while the plugin is | |
| 347 | + // still mid-teardown and has to render against half-cleared state. | |
| 33 | 348 | self::clear_plugins_cache(); |
| 34 | - self::clear_transient_cache(); | |
| 35 | 349 | } |
| 36 | 350 | /** |
| 37 | 351 | * Clear the template cache. |
| 38 | 352 | * |
| @@ -53,8 +367,11 @@ | ||
| 53 | 367 | } |
| 54 | 368 | if ( method_exists( 'LiteSpeed_Cache_API', 'purge_all' ) ) { |
| 55 | 369 | \LiteSpeed_Cache_API::purge_all(); |
| 56 | 370 | } |
| 371 | + if ( class_exists( '\LiteSpeed\Purge' ) ) { | |
| 372 | + \LiteSpeed\Purge::purge_all(); | |
| 373 | + } | |
| 57 | 374 | if ( class_exists( 'Endurance_Page_Cache' ) ) { |
| 58 | 375 | $epc = new \Endurance_Page_Cache(); |
| 59 | 376 | $epc->purge_all(); |
| 60 | 377 | } |
| @@ -126,16 +443,15 @@ | ||
| 126 | 443 | do_action( 'ce_clear_cache' ); |
| 127 | 444 | } |
| 128 | 445 | // When plugins have a simple method, add them to the array ('Plugin Name' => 'method_name'). |
| 129 | 446 | $others = [ |
| 130 | - 'WP Fastest Cache' => 'wpfc_clear_all_cache', | |
| 131 | - 'Cachify' => 'cachify_flush_cache', | |
| 132 | - 'Comet Cache' => [ 'comet_cache', 'clear' ], | |
| 133 | - 'SG Optimizer' => 'sg_cachepress_purge_cache', | |
| 134 | - 'Pantheon' => 'pantheon_wp_clear_edge_all', | |
| 135 | - 'Zen Cache' => [ 'zencache', 'clear' ], | |
| 136 | - 'Breeze' => [ 'Breeze_PurgeCache', 'breeze_cache_flush' ], | |
| 137 | - 'Swift Performance' => [ 'Swift_Performance_Cache', 'clear_all_cache' ], | |
| 447 | + 'WP Fastest Cache' => 'wpfc_clear_all_cache', | |
| 448 | + 'Cachify' => 'cachify_flush_cache', | |
| 449 | + 'Comet Cache' => [ 'comet_cache', 'clear' ], | |
| 450 | + 'SG Optimizer' => 'sg_cachepress_purge_cache', | |
| 451 | + 'Pantheon' => 'pantheon_wp_clear_edge_all', | |
| 452 | + 'Zen Cache' => [ 'zencache', 'clear' ], | |
| 453 | + 'Breeze' => [ 'Breeze_PurgeCache', 'breeze_cache_flush' ], | |
| 138 | 454 | ]; |
| 139 | 455 | foreach ( $others as $plugin => $method ) { |
| 140 | 456 | if ( is_callable( $method ) ) { |
| 141 | 457 | call_user_func( $method ); |
| @@ -154,16 +470,17 @@ | ||
| 154 | 470 | * Purge GoDaddy Managed WordPress Hosting (Varnish) |
| 155 | 471 | * |
| 156 | 472 | * Source: https://github.com/wp-media/wp-rocket/blob/master/inc/3rd-party/hosting/godaddy.php |
| 157 | 473 | * |
| 158 | - * @param String $method | |
| 159 | - * @param String|Null $url | |
| 474 | + * @param string $method The request method. | |
| 475 | + * @param string $url The request URL. | |
| 476 | + * | |
| 477 | + * @return void | |
| 160 | 478 | */ |
| 161 | 479 | public static function godaddy_request( $method, $url = null ) { |
| 162 | 480 | $url = empty( $url ) ? home_url() : $url; |
| 163 | 481 | $host = wp_parse_url( $url, PHP_URL_HOST ); |
| 164 | 482 | $url = set_url_scheme( str_replace( $host, \WPaas\Plugin::vip(), $url ), 'http' ); |
| 165 | - wp_cache_flush(); | |
| 166 | 483 | update_option( 'gd_system_last_cache_flush', time() ); // purge apc. |
| 167 | 484 | wp_remote_request( |
| 168 | 485 | esc_url_raw( $url ), |
| 169 | 486 | [ |
| @@ -181,16 +498,18 @@ | ||
| 181 | 498 | * @param string $cache_key Object cache key. |
| 182 | 499 | * @param string $template Located template. |
| 183 | 500 | */ |
| 184 | 501 | public static function set_template_cache( $cache_key, $template ) { |
| 185 | - wp_cache_set( $cache_key, $template, 'shopbuilder' ); | |
| 186 | - $cached_templates = wp_cache_get( 'shopbuilder_cached_templates', 'shopbuilder' ); | |
| 502 | + wp_cache_set( $cache_key, $template, self::group(), 12 * HOUR_IN_SECONDS ); | |
| 503 | + $cached_templates = wp_cache_get( 'shopbuilder_cached_templates', self::group() ); | |
| 187 | 504 | if ( is_array( $cached_templates ) ) { |
| 188 | 505 | $cached_templates[] = $cache_key; |
| 189 | 506 | } else { |
| 190 | 507 | $cached_templates = [ $cache_key ]; |
| 191 | 508 | } |
| 192 | - wp_cache_set( 'shopbuilder_cached_templates', $cached_templates, 'shopbuilder' ); | |
| 509 | + // The index must outlive the entries it tracks so a purge can still find | |
| 510 | + // them, hence the longer lifetime than the templates themselves. | |
| 511 | + wp_cache_set( 'shopbuilder_cached_templates', $cached_templates, self::group(), WEEK_IN_SECONDS ); | |
| 193 | 512 | } |
| 194 | 513 | /** |
| 195 | 514 | * Clear the template cache. |
| 196 | 515 | * |
| @@ -196,14 +515,14 @@ | ||
| 196 | 515 | * |
| 197 | 516 | * @since 4.3.0 |
| 198 | 517 | */ |
| 199 | 518 | public static function clear_template_cache() { |
| 200 | - $cached_templates = wp_cache_get( 'shopbuilder_cached_templates', 'shopbuilder' ); | |
| 519 | + $cached_templates = wp_cache_get( 'shopbuilder_cached_templates', self::group() ); | |
| 201 | 520 | if ( is_array( $cached_templates ) ) { |
| 202 | 521 | foreach ( $cached_templates as $cache_key ) { |
| 203 | - wp_cache_delete( $cache_key, 'shopbuilder' ); | |
| 522 | + wp_cache_delete( $cache_key, self::group() ); | |
| 204 | 523 | } |
| 205 | - wp_cache_delete( 'shopbuilder_cached_templates', 'shopbuilder' ); | |
| 524 | + wp_cache_delete( 'shopbuilder_cached_templates', self::group() ); | |
| 206 | 525 | } |
| 207 | 526 | } |
| 208 | 527 | /** |
| 209 | 528 | * Added data cache. |
| @@ -209,18 +528,21 @@ | ||
| 209 | 528 | * Added data cache. |
| 210 | 529 | * |
| 211 | 530 | * @since 4.3.0 |
| 212 | 531 | * @param string $cache_key Object cache key. |
| 213 | - * @param string $data Located template. | |
| 532 | + * | |
| 533 | + * @return void | |
| 214 | 534 | */ |
| 215 | 535 | public static function set_data_cache_key( $cache_key ) { |
| 216 | - $cached_data = wp_cache_get( 'shopbuilder_cached_data', 'shopbuilder' ); | |
| 536 | + $cached_data = wp_cache_get( 'shopbuilder_cached_data', self::group() ); | |
| 217 | 537 | if ( is_array( $cached_data ) ) { |
| 218 | 538 | $cached_data[] = $cache_key; |
| 219 | 539 | } else { |
| 220 | 540 | $cached_data = [ $cache_key ]; |
| 221 | 541 | } |
| 222 | - wp_cache_set( 'shopbuilder_cached_data', $cached_data, 'shopbuilder' ); | |
| 542 | + // The index must outlive the entries it tracks so a purge can still find | |
| 543 | + // them, hence the longer lifetime than the cached data itself. | |
| 544 | + wp_cache_set( 'shopbuilder_cached_data', $cached_data, self::group(), WEEK_IN_SECONDS ); | |
| 223 | 545 | } |
| 224 | 546 | /** |
| 225 | 547 | * Clear data cache. |
| 226 | 548 | * |
| @@ -226,46 +548,186 @@ | ||
| 226 | 548 | * |
| 227 | 549 | * @since 4.3.0 |
| 228 | 550 | */ |
| 229 | 551 | public static function clear_data_cache() { |
| 230 | - $cached_templates = wp_cache_get( 'shopbuilder_cached_data', 'shopbuilder' ); | |
| 552 | + $cached_templates = wp_cache_get( 'shopbuilder_cached_data', self::group() ); | |
| 231 | 553 | if ( is_array( $cached_templates ) ) { |
| 232 | 554 | foreach ( $cached_templates as $cache_key ) { |
| 233 | - wp_cache_delete( $cache_key, 'shopbuilder' ); | |
| 555 | + wp_cache_delete( $cache_key, self::group() ); | |
| 234 | 556 | } |
| 235 | - wp_cache_delete( 'shopbuilder_cached_data', 'shopbuilder' ); | |
| 557 | + wp_cache_delete( 'shopbuilder_cached_data', self::group() ); | |
| 236 | 558 | } |
| 237 | 559 | } |
| 238 | 560 | |
| 561 | + /** | |
| 562 | + * Clear asset cache. | |
| 563 | + * | |
| 564 | + * @return void | |
| 565 | + */ | |
| 566 | + public static function clear_asset_cache() { | |
| 567 | + if ( ! Fns::is_optimization_enabled() ) { | |
| 568 | + return; | |
| 569 | + } | |
| 239 | 570 | |
| 571 | + $upload_dir = wp_upload_dir(); | |
| 572 | + $asset_dir = trailingslashit( $upload_dir['basedir'] ) . 'shopbuilder_uploads/cache/'; | |
| 573 | + | |
| 574 | + // Delete old assets. | |
| 575 | + if ( is_dir( $asset_dir ) ) { | |
| 576 | + self::delete_dir_contents( $asset_dir ); | |
| 577 | + } | |
| 578 | + | |
| 579 | + // Re-generate assets. | |
| 580 | + AssetRegistry::instance()->regenerate_bundles(); | |
| 581 | + } | |
| 582 | + | |
| 240 | 583 | /** |
| 241 | - * Added data cache. | |
| 584 | + * Recursively delete directory contents. | |
| 242 | 585 | * |
| 586 | + * @param string $dir Directory path. | |
| 587 | + * @return void | |
| 588 | + */ | |
| 589 | + public static function delete_dir_contents( $dir ) { | |
| 590 | + global $wp_filesystem; | |
| 591 | + | |
| 592 | + if ( ! function_exists( 'WP_Filesystem' ) ) { | |
| 593 | + require_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 594 | + } | |
| 595 | + | |
| 596 | + if ( ! $wp_filesystem ) { | |
| 597 | + WP_Filesystem(); | |
| 598 | + } | |
| 599 | + | |
| 600 | + if ( ! $wp_filesystem->is_dir( $dir ) ) { | |
| 601 | + return; | |
| 602 | + } | |
| 603 | + | |
| 604 | + $contents = $wp_filesystem->dirlist( $dir ); | |
| 605 | + | |
| 606 | + if ( ! is_array( $contents ) ) { | |
| 607 | + return; | |
| 608 | + } | |
| 609 | + | |
| 610 | + foreach ( $contents as $item ) { | |
| 611 | + $path = trailingslashit( $dir ) . $item['name']; | |
| 612 | + | |
| 613 | + if ( 'f' === $item['type'] ) { | |
| 614 | + $wp_filesystem->delete( $path, false ); | |
| 615 | + } elseif ( 'd' === $item['type'] ) { | |
| 616 | + self::delete_dir_contents( $path ); | |
| 617 | + | |
| 618 | + $wp_filesystem->delete( $path, true ); | |
| 619 | + } | |
| 620 | + } | |
| 621 | + } | |
| 622 | + | |
| 623 | + | |
| 624 | + /** | |
| 625 | + * Register a transient key with the purge index. | |
| 626 | + * | |
| 627 | + * @deprecated No longer required. Purges now resolve the plugin's transients | |
| 628 | + * from wp_options by key prefix, see TRANSIENT_PREFIXES. | |
| 629 | + * | |
| 630 | + * Maintaining the index meant reading, appending to, deduplicating and | |
| 631 | + * rewriting the whole list of keys on every frontend cache miss. That is | |
| 632 | + * O(n) per write, so rebuilding it after an update wiped it cost O(n^2): | |
| 633 | + * measured at 13.8 seconds of CPU for a 10,000 product catalogue. It was | |
| 634 | + * also lossy under concurrency, because the read-modify-write was unguarded. | |
| 635 | + * | |
| 636 | + * Kept as a no-op so third-party callers do not fatal. | |
| 637 | + * | |
| 243 | 638 | * @since 4.3.0 |
| 244 | 639 | * @param string $cache_key Transient cache key. |
| 245 | 640 | */ |
| 246 | 641 | public static function set_transient_cache_key( $cache_key ) { |
| 247 | - $cached_data = get_transient( 'shopbuilder_transient_cached_data' ); | |
| 248 | - if ( is_array( $cached_data ) ) { | |
| 249 | - $cached_data[] = $cache_key; | |
| 250 | - } else { | |
| 251 | - $cached_data = [ $cache_key ]; | |
| 252 | - } | |
| 253 | - | |
| 254 | - set_transient( 'shopbuilder_transient_cached_data', array_unique( $cached_data ) ); | |
| 642 | + // Intentionally empty. Retained so third-party code calling this does not | |
| 643 | + // fatal; ShopBuilder itself no longer calls it. | |
| 644 | + unset( $cache_key ); | |
| 255 | 645 | } |
| 256 | 646 | |
| 257 | 647 | /** |
| 258 | - * Clear data cache. | |
| 648 | + * Clear every transient owned by the plugin. | |
| 259 | 649 | * |
| 260 | 650 | * @since 4.3.0 |
| 651 | + * | |
| 652 | + * @return int Number of transients deleted. | |
| 261 | 653 | */ |
| 262 | 654 | public static function clear_transient_cache() { |
| 263 | - $cached_templates = get_transient( 'shopbuilder_transient_cached_data' ); | |
| 264 | - if ( is_array( $cached_templates ) ) { | |
| 265 | - foreach ( $cached_templates as $cache_key ) { | |
| 266 | - delete_transient( $cache_key ); | |
| 655 | + $deleted = self::delete_transients_by_prefix( self::TRANSIENT_PREFIXES ); | |
| 656 | + | |
| 657 | + /* | |
| 658 | + * Nothing writes the index any more, but installations upgrading from an | |
| 659 | + * earlier version still hold one, and it can be megabytes on a large | |
| 660 | + * catalogue. Its key predates the rtsb_ namespace so no prefix matches | |
| 661 | + * it; remove it explicitly. Harmless when already absent. | |
| 662 | + */ | |
| 663 | + delete_transient( self::TRANSIENT_INDEX_KEY ); | |
| 664 | + | |
| 665 | + return $deleted; | |
| 666 | + } | |
| 667 | + | |
| 668 | + /** | |
| 669 | + * Delete every transient whose key starts with one of the given prefixes. | |
| 670 | + * | |
| 671 | + * Runs one indexed lookup against wp_options per prefix and deletes through | |
| 672 | + * delete_transient(), so the timeout row is removed with its value and any | |
| 673 | + * persistent object cache stays consistent. | |
| 674 | + * | |
| 675 | + * Only ever called from a purge. It must not be reachable from a normal | |
| 676 | + * frontend request. | |
| 677 | + * | |
| 678 | + * @param string[] $prefixes Transient key prefixes, without the _transient_ part. | |
| 679 | + * | |
| 680 | + * @return int Number of transients deleted. | |
| 681 | + */ | |
| 682 | + public static function delete_transients_by_prefix( array $prefixes ) { | |
| 683 | + global $wpdb; | |
| 684 | + | |
| 685 | + $deleted = 0; | |
| 686 | + | |
| 687 | + foreach ( $prefixes as $prefix ) { | |
| 688 | + if ( '' === $prefix ) { | |
| 689 | + continue; | |
| 267 | 690 | } |
| 268 | - delete_transient( 'shopbuilder_transient_cached_data' ); | |
| 691 | + | |
| 692 | + /* | |
| 693 | + * esc_like() is applied to the whole literal, including the | |
| 694 | + * _transient_ part: the underscore is a single character wildcard in | |
| 695 | + * LIKE, so an unescaped '_transient_rtsb_' would also match keys such | |
| 696 | + * as 'Xtransient_rtsbY'. | |
| 697 | + */ | |
| 698 | + $like = $wpdb->esc_like( '_transient_' . $prefix ) . '%'; | |
| 699 | + | |
| 700 | + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching | |
| 701 | + $option_names = $wpdb->get_col( | |
| 702 | + $wpdb->prepare( | |
| 703 | + "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", | |
| 704 | + $like | |
| 705 | + ) | |
| 706 | + ); | |
| 707 | + | |
| 708 | + foreach ( $option_names as $option_name ) { | |
| 709 | + // Trim only the leading marker; str_replace() would also corrupt | |
| 710 | + // a key that happens to contain '_transient_' further along. | |
| 711 | + $key = substr( $option_name, strlen( '_transient_' ) ); | |
| 712 | + | |
| 713 | + if ( '' === $key ) { | |
| 714 | + continue; | |
| 715 | + } | |
| 716 | + | |
| 717 | + delete_transient( $key ); | |
| 718 | + ++$deleted; | |
| 719 | + } | |
| 269 | 720 | } |
| 721 | + | |
| 722 | + return $deleted; | |
| 723 | + } | |
| 724 | + | |
| 725 | + /** | |
| 726 | + * Clear all theme css handle transients. | |
| 727 | + * | |
| 728 | + * @return void | |
| 729 | + */ | |
| 730 | + public static function delete_all_theme_css_handle_transients() { | |
| 731 | + self::delete_transients_by_prefix( [ 'rtsb_theme_css_handle_' ] ); | |
| 270 | 732 | } |
| 271 | 733 | } |