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
← All changes | app/Helpers/Cache.php +502 -40 2.2.1 → 3.4.2 View file →
@@ -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 }