| 1 |
<?php |
| 2 |
/** |
| 3 |
* Shortcodes Class. |
| 4 |
* |
| 5 |
* This class contains all the Shortcodes. |
| 6 |
* |
| 7 |
* @package RadiusTheme\SB |
| 8 |
*/ |
| 9 |
|
| 10 |
namespace RadiusTheme\SB\Helpers; |
| 11 |
|
| 12 |
use W3TC\Dispatcher; |
| 13 |
use RadiusTheme\SB\Traits\SingletonTrait; |
| 14 |
use RadiusTheme\SB\Controllers\AssetRegistry; |
| 15 |
|
| 16 |
// Do not allow directly accessing this file. |
| 17 |
if ( ! defined( 'ABSPATH' ) ) { |
| 18 |
exit( 'This script cannot be accessed directly.' ); |
| 19 |
} |
| 20 |
|
| 21 |
/** |
| 22 |
* CacheController Class. |
| 23 |
*/ |
| 24 |
class Cache { |
| 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 |
/** |
| 328 |
* Clear the template cache. |
| 329 |
* |
| 330 |
* @since 4.3.0 |
| 331 |
*/ |
| 332 |
public static function clear_all_cache() { |
| 333 |
if ( self::$all_cache_cleared ) { |
| 334 |
return; |
| 335 |
} |
| 336 |
|
| 337 |
self::$all_cache_cleared = true; |
| 338 |
|
| 339 |
self::clear_data_cache(); |
| 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. |
| 348 |
self::clear_plugins_cache(); |
| 349 |
} |
| 350 |
/** |
| 351 |
* Clear the template cache. |
| 352 |
* |
| 353 |
* @since 4.3.0 |
| 354 |
*/ |
| 355 |
public static function clear_plugins_cache() { |
| 356 |
// Clear W3 Total Cache. |
| 357 |
if ( function_exists( 'w3tc_flush_all' ) ) { |
| 358 |
w3tc_flush_all(); |
| 359 |
} |
| 360 |
// Clear WP Super Cache. |
| 361 |
if ( function_exists( 'wp_cache_clear_cache' ) ) { |
| 362 |
wp_cache_clear_cache(); |
| 363 |
} |
| 364 |
// Clear WP Rocket cache. |
| 365 |
if ( function_exists( 'rocket_clean_domain' ) ) { |
| 366 |
rocket_clean_domain(); |
| 367 |
} |
| 368 |
if ( method_exists( 'LiteSpeed_Cache_API', 'purge_all' ) ) { |
| 369 |
\LiteSpeed_Cache_API::purge_all(); |
| 370 |
} |
| 371 |
if ( class_exists( '\LiteSpeed\Purge' ) ) { |
| 372 |
\LiteSpeed\Purge::purge_all(); |
| 373 |
} |
| 374 |
if ( class_exists( 'Endurance_Page_Cache' ) ) { |
| 375 |
$epc = new \Endurance_Page_Cache(); |
| 376 |
$epc->purge_all(); |
| 377 |
} |
| 378 |
if ( class_exists( 'SG_CachePress_Supercacher' ) && method_exists( 'SG_CachePress_Supercacher', 'purge_cache' ) ) { |
| 379 |
\SG_CachePress_Supercacher::purge_cache( true ); |
| 380 |
} |
| 381 |
if ( class_exists( 'SiteGround_Optimizer\Supercacher\Supercacher' ) ) { |
| 382 |
\SiteGround_Optimizer\Supercacher\Supercacher::purge_cache(); |
| 383 |
} |
| 384 |
if ( isset( $GLOBALS['wp_fastest_cache'] ) && method_exists( $GLOBALS['wp_fastest_cache'], 'deleteCache' ) ) { |
| 385 |
$GLOBALS['wp_fastest_cache']->deleteCache( true ); |
| 386 |
} |
| 387 |
if ( is_callable( [ 'Swift_Performance_Cache', 'clear_all_cache' ] ) ) { |
| 388 |
\Swift_Performance_Cache::clear_all_cache(); |
| 389 |
} |
| 390 |
if ( is_callable( [ 'Hummingbird\WP_Hummingbird', 'flush_cache' ] ) ) { |
| 391 |
\Hummingbird\WP_Hummingbird::flush_cache( true, false ); |
| 392 |
} |
| 393 |
if ( class_exists( 'WP_Optimize' ) ) { |
| 394 |
\WP_Optimize()->get_page_cache()->purge(); |
| 395 |
} |
| 396 |
|
| 397 |
// Purge WP Engine. |
| 398 |
if ( class_exists( 'WpeCommon' ) ) { |
| 399 |
if ( method_exists( 'WpeCommon', 'purge_memcached' ) ) { |
| 400 |
\WpeCommon::purge_memcached(); |
| 401 |
} |
| 402 |
if ( method_exists( 'WpeCommon', 'clear_maxcdn_cache' ) ) { |
| 403 |
\WpeCommon::clear_maxcdn_cache(); |
| 404 |
} |
| 405 |
if ( method_exists( 'WpeCommon', 'purge_varnish_cache' ) ) { |
| 406 |
\WpeCommon::purge_varnish_cache(); |
| 407 |
} |
| 408 |
} |
| 409 |
// Purge Kinsta. |
| 410 |
global $kinsta_cache; |
| 411 |
if ( isset( $kinsta_cache ) && class_exists( '\\Kinsta\\CDN_Enabler' ) ) { |
| 412 |
if ( ! empty( $kinsta_cache->kinsta_cache_purge ) && is_callable( [ $kinsta_cache->kinsta_cache_purge, 'purge_complete_caches' ] ) ) { |
| 413 |
$kinsta_cache->kinsta_cache_purge->purge_complete_caches(); |
| 414 |
} |
| 415 |
} |
| 416 |
// Purge Pagely. |
| 417 |
if ( class_exists( 'PagelyCachePurge' ) ) { |
| 418 |
$purge_pagely = new \PagelyCachePurge(); |
| 419 |
if ( is_callable( [ $purge_pagely, 'purgeAll' ] ) ) { |
| 420 |
$purge_pagely->purgeAll(); |
| 421 |
} |
| 422 |
} |
| 423 |
// Purge Pressidum. |
| 424 |
if ( defined( 'WP_NINUKIS_WP_NAME' ) && class_exists( 'Ninukis_Plugin' ) && is_callable( [ 'Ninukis_Plugin', 'get_instance' ] ) ) { |
| 425 |
$purge_pressidum = \Ninukis_Plugin::get_instance(); |
| 426 |
if ( is_callable( [ $purge_pressidum, 'purgeAllCaches' ] ) ) { |
| 427 |
$purge_pressidum->purgeAllCaches(); |
| 428 |
} |
| 429 |
} |
| 430 |
// Purge Savvii. |
| 431 |
if ( defined( '\Savvii\CacheFlusherPlugin::NAME_DOMAINFLUSH_NOW' ) ) { |
| 432 |
$purge_savvii = new \Savvii\CacheFlusherPlugin(); |
| 433 |
if ( is_callable( [ $purge_savvii, 'domainflush' ] ) ) { |
| 434 |
$purge_savvii->domainflush(); |
| 435 |
} |
| 436 |
} |
| 437 |
// Purge Hyper Cache. |
| 438 |
if ( class_exists( 'HyperCache' ) ) { |
| 439 |
do_action( 'autoptimize_action_cachepurged' ); |
| 440 |
} |
| 441 |
// purge cache enabler. |
| 442 |
if ( has_action( 'ce_clear_cache' ) ) { |
| 443 |
do_action( 'ce_clear_cache' ); |
| 444 |
} |
| 445 |
// When plugins have a simple method, add them to the array ('Plugin Name' => 'method_name'). |
| 446 |
$others = [ |
| 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' ], |
| 454 |
]; |
| 455 |
foreach ( $others as $plugin => $method ) { |
| 456 |
if ( is_callable( $method ) ) { |
| 457 |
call_user_func( $method ); |
| 458 |
} |
| 459 |
} |
| 460 |
// Purge Godaddy Managed WordPress Hosting (Varnish + APC). |
| 461 |
if ( class_exists( 'WPaaS\Plugin' ) ) { |
| 462 |
self::godaddy_request( 'BAN' ); |
| 463 |
} |
| 464 |
|
| 465 |
wp_cache_flush(); |
| 466 |
} |
| 467 |
|
| 468 |
|
| 469 |
/** |
| 470 |
* Purge GoDaddy Managed WordPress Hosting (Varnish) |
| 471 |
* |
| 472 |
* Source: https://github.com/wp-media/wp-rocket/blob/master/inc/3rd-party/hosting/godaddy.php |
| 473 |
* |
| 474 |
* @param string $method The request method. |
| 475 |
* @param string $url The request URL. |
| 476 |
* |
| 477 |
* @return void |
| 478 |
*/ |
| 479 |
public static function godaddy_request( $method, $url = null ) { |
| 480 |
$url = empty( $url ) ? home_url() : $url; |
| 481 |
$host = wp_parse_url( $url, PHP_URL_HOST ); |
| 482 |
$url = set_url_scheme( str_replace( $host, \WPaas\Plugin::vip(), $url ), 'http' ); |
| 483 |
update_option( 'gd_system_last_cache_flush', time() ); // purge apc. |
| 484 |
wp_remote_request( |
| 485 |
esc_url_raw( $url ), |
| 486 |
[ |
| 487 |
'method' => $method, |
| 488 |
'blocking' => false, |
| 489 |
'headers' => [ 'Host' => $host ], |
| 490 |
] |
| 491 |
); |
| 492 |
} |
| 493 |
|
| 494 |
/** |
| 495 |
* Add a template to the template cache. |
| 496 |
* |
| 497 |
* @since 4.3.0 |
| 498 |
* @param string $cache_key Object cache key. |
| 499 |
* @param string $template Located template. |
| 500 |
*/ |
| 501 |
public static function set_template_cache( $cache_key, $template ) { |
| 502 |
wp_cache_set( $cache_key, $template, self::group(), 12 * HOUR_IN_SECONDS ); |
| 503 |
$cached_templates = wp_cache_get( 'shopbuilder_cached_templates', self::group() ); |
| 504 |
if ( is_array( $cached_templates ) ) { |
| 505 |
$cached_templates[] = $cache_key; |
| 506 |
} else { |
| 507 |
$cached_templates = [ $cache_key ]; |
| 508 |
} |
| 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 ); |
| 512 |
} |
| 513 |
/** |
| 514 |
* Clear the template cache. |
| 515 |
* |
| 516 |
* @since 4.3.0 |
| 517 |
*/ |
| 518 |
public static function clear_template_cache() { |
| 519 |
$cached_templates = wp_cache_get( 'shopbuilder_cached_templates', self::group() ); |
| 520 |
if ( is_array( $cached_templates ) ) { |
| 521 |
foreach ( $cached_templates as $cache_key ) { |
| 522 |
wp_cache_delete( $cache_key, self::group() ); |
| 523 |
} |
| 524 |
wp_cache_delete( 'shopbuilder_cached_templates', self::group() ); |
| 525 |
} |
| 526 |
} |
| 527 |
/** |
| 528 |
* Added data cache. |
| 529 |
* |
| 530 |
* @since 4.3.0 |
| 531 |
* @param string $cache_key Object cache key. |
| 532 |
* |
| 533 |
* @return void |
| 534 |
*/ |
| 535 |
public static function set_data_cache_key( $cache_key ) { |
| 536 |
$cached_data = wp_cache_get( 'shopbuilder_cached_data', self::group() ); |
| 537 |
if ( is_array( $cached_data ) ) { |
| 538 |
$cached_data[] = $cache_key; |
| 539 |
} else { |
| 540 |
$cached_data = [ $cache_key ]; |
| 541 |
} |
| 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 ); |
| 545 |
} |
| 546 |
/** |
| 547 |
* Clear data cache. |
| 548 |
* |
| 549 |
* @since 4.3.0 |
| 550 |
*/ |
| 551 |
public static function clear_data_cache() { |
| 552 |
$cached_templates = wp_cache_get( 'shopbuilder_cached_data', self::group() ); |
| 553 |
if ( is_array( $cached_templates ) ) { |
| 554 |
foreach ( $cached_templates as $cache_key ) { |
| 555 |
wp_cache_delete( $cache_key, self::group() ); |
| 556 |
} |
| 557 |
wp_cache_delete( 'shopbuilder_cached_data', self::group() ); |
| 558 |
} |
| 559 |
} |
| 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 |
} |
| 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 |
|
| 583 |
/** |
| 584 |
* Recursively delete directory contents. |
| 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 |
* |
| 638 |
* @since 4.3.0 |
| 639 |
* @param string $cache_key Transient cache key. |
| 640 |
*/ |
| 641 |
public static function set_transient_cache_key( $cache_key ) { |
| 642 |
// Intentionally empty. Retained so third-party code calling this does not |
| 643 |
// fatal; ShopBuilder itself no longer calls it. |
| 644 |
unset( $cache_key ); |
| 645 |
} |
| 646 |
|
| 647 |
/** |
| 648 |
* Clear every transient owned by the plugin. |
| 649 |
* |
| 650 |
* @since 4.3.0 |
| 651 |
* |
| 652 |
* @return int Number of transients deleted. |
| 653 |
*/ |
| 654 |
public static function clear_transient_cache() { |
| 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; |
| 690 |
} |
| 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 |
} |
| 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_' ] ); |
| 732 |
} |
| 733 |
} |
| 734 |
|