PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.4.2
ShopBuilder – WooCommerce Builder For Elementor v3.4.2
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
shopbuilder / app / Helpers / Cache.php

Cache.php in ShopBuilder – WooCommerce Builder For Elementor 3.4.2, at app/Helpers/Cache.php

734 lines 22.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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