PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.2.5
ShopBuilder – WooCommerce Builder For Elementor v3.2.5
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 +55 -422 3.4.23.2.5 View file →
@@ -23,330 +23,18 @@
23 23 */
24 24 class Cache {
25 25
26 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 27 * Clear the template cache.
329 28 *
330 29 * @since 4.3.0
331 30 */
332 31 public static function clear_all_cache() {
333 - if ( self::$all_cache_cleared ) {
334 - return;
335 - }
336 -
337 - self::$all_cache_cleared = true;
338 -
339 32 self::clear_data_cache();
340 33 self::clear_template_cache();
34 + self::clear_plugins_cache();
341 35 self::clear_transient_cache();
342 36 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 37 }
350 38 /**
351 39 * Clear the template cache.
352 40 *
@@ -367,11 +55,8 @@
367 55 }
368 56 if ( method_exists( 'LiteSpeed_Cache_API', 'purge_all' ) ) {
369 57 \LiteSpeed_Cache_API::purge_all();
370 58 }
371 - if ( class_exists( '\LiteSpeed\Purge' ) ) {
372 - \LiteSpeed\Purge::purge_all();
373 - }
374 59 if ( class_exists( 'Endurance_Page_Cache' ) ) {
375 60 $epc = new \Endurance_Page_Cache();
376 61 $epc->purge_all();
377 62 }
@@ -443,15 +128,16 @@
443 128 do_action( 'ce_clear_cache' );
444 129 }
445 130 // When plugins have a simple method, add them to the array ('Plugin Name' => 'method_name').
446 131 $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' ],
132 + 'WP Fastest Cache' => 'wpfc_clear_all_cache',
133 + 'Cachify' => 'cachify_flush_cache',
134 + 'Comet Cache' => [ 'comet_cache', 'clear' ],
135 + 'SG Optimizer' => 'sg_cachepress_purge_cache',
136 + 'Pantheon' => 'pantheon_wp_clear_edge_all',
137 + 'Zen Cache' => [ 'zencache', 'clear' ],
138 + 'Breeze' => [ 'Breeze_PurgeCache', 'breeze_cache_flush' ],
139 + 'Swift Performance' => [ 'Swift_Performance_Cache', 'clear_all_cache' ],
454 140 ];
455 141 foreach ( $others as $plugin => $method ) {
456 142 if ( is_callable( $method ) ) {
457 143 call_user_func( $method );
@@ -479,8 +165,9 @@
479 165 public static function godaddy_request( $method, $url = null ) {
480 166 $url = empty( $url ) ? home_url() : $url;
481 167 $host = wp_parse_url( $url, PHP_URL_HOST );
482 168 $url = set_url_scheme( str_replace( $host, \WPaas\Plugin::vip(), $url ), 'http' );
169 + wp_cache_flush();
483 170 update_option( 'gd_system_last_cache_flush', time() ); // purge apc.
484 171 wp_remote_request(
485 172 esc_url_raw( $url ),
486 173 [
@@ -498,18 +185,16 @@
498 185 * @param string $cache_key Object cache key.
499 186 * @param string $template Located template.
500 187 */
501 188 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() );
189 + wp_cache_set( $cache_key, $template, 'shopbuilder' );
190 + $cached_templates = wp_cache_get( 'shopbuilder_cached_templates', 'shopbuilder' );
504 191 if ( is_array( $cached_templates ) ) {
505 192 $cached_templates[] = $cache_key;
506 193 } else {
507 194 $cached_templates = [ $cache_key ];
508 195 }
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 );
196 + wp_cache_set( 'shopbuilder_cached_templates', $cached_templates, 'shopbuilder' );
512 197 }
513 198 /**
514 199 * Clear the template cache.
515 200 *
@@ -515,14 +200,14 @@
515 200 *
516 201 * @since 4.3.0
517 202 */
518 203 public static function clear_template_cache() {
519 - $cached_templates = wp_cache_get( 'shopbuilder_cached_templates', self::group() );
204 + $cached_templates = wp_cache_get( 'shopbuilder_cached_templates', 'shopbuilder' );
520 205 if ( is_array( $cached_templates ) ) {
521 206 foreach ( $cached_templates as $cache_key ) {
522 - wp_cache_delete( $cache_key, self::group() );
207 + wp_cache_delete( $cache_key, 'shopbuilder' );
523 208 }
524 - wp_cache_delete( 'shopbuilder_cached_templates', self::group() );
209 + wp_cache_delete( 'shopbuilder_cached_templates', 'shopbuilder' );
525 210 }
526 211 }
527 212 /**
528 213 * Added data cache.
@@ -532,17 +217,15 @@
532 217 *
533 218 * @return void
534 219 */
535 220 public static function set_data_cache_key( $cache_key ) {
536 - $cached_data = wp_cache_get( 'shopbuilder_cached_data', self::group() );
221 + $cached_data = wp_cache_get( 'shopbuilder_cached_data', 'shopbuilder' );
537 222 if ( is_array( $cached_data ) ) {
538 223 $cached_data[] = $cache_key;
539 224 } else {
540 225 $cached_data = [ $cache_key ];
541 226 }
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 );
227 + wp_cache_set( 'shopbuilder_cached_data', $cached_data, 'shopbuilder' );
545 228 }
546 229 /**
547 230 * Clear data cache.
548 231 *
@@ -548,14 +231,14 @@
548 231 *
549 232 * @since 4.3.0
550 233 */
551 234 public static function clear_data_cache() {
552 - $cached_templates = wp_cache_get( 'shopbuilder_cached_data', self::group() );
235 + $cached_templates = wp_cache_get( 'shopbuilder_cached_data', 'shopbuilder' );
553 236 if ( is_array( $cached_templates ) ) {
554 237 foreach ( $cached_templates as $cache_key ) {
555 - wp_cache_delete( $cache_key, self::group() );
238 + wp_cache_delete( $cache_key, 'shopbuilder' );
556 239 }
557 - wp_cache_delete( 'shopbuilder_cached_data', self::group() );
240 + wp_cache_delete( 'shopbuilder_cached_data', 'shopbuilder' );
558 241 }
559 242 }
560 243
561 244 /**
@@ -621,113 +304,63 @@
621 304 }
622 305
623 306
624 307 /**
625 - * Register a transient key with the purge index.
308 + * Added data cache.
626 309 *
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 310 * @since 4.3.0
639 311 * @param string $cache_key Transient cache key.
640 312 */
641 313 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 );
314 + $cached_data = get_transient( 'shopbuilder_transient_cached_data' );
315 + if ( is_array( $cached_data ) ) {
316 + $cached_data[] = $cache_key;
317 + } else {
318 + $cached_data = [ $cache_key ];
319 + }
320 +
321 + set_transient( 'shopbuilder_transient_cached_data', array_unique( $cached_data ) );
645 322 }
646 323
647 324 /**
648 - * Clear every transient owned by the plugin.
325 + * Clear data cache.
649 326 *
650 327 * @since 4.3.0
651 - *
652 - * @return int Number of transients deleted.
653 328 */
654 329 public static function clear_transient_cache() {
655 - $deleted = self::delete_transients_by_prefix( self::TRANSIENT_PREFIXES );
330 + $cached_templates = get_transient( 'shopbuilder_transient_cached_data' );
331 + if ( is_array( $cached_templates ) ) {
332 + foreach ( $cached_templates as $cache_key ) {
333 + delete_transient( $cache_key );
334 + }
335 + delete_transient( 'shopbuilder_transient_cached_data' );
336 + }
656 337
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;
338 + self::delete_all_theme_css_handle_transients();
666 339 }
667 340
668 341 /**
669 - * Delete every transient whose key starts with one of the given prefixes.
342 + * Clear all theme css handle transients.
670 343 *
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.
344 + * @return void
681 345 */
682 - public static function delete_transients_by_prefix( array $prefixes ) {
346 + public static function delete_all_theme_css_handle_transients() {
683 347 global $wpdb;
684 348
685 - $deleted = 0;
349 + $prefix = '_transient_';
350 + $transient_ns = 'rtsb_theme_css_handle_';
686 351
687 - foreach ( $prefixes as $prefix ) {
688 - if ( '' === $prefix ) {
689 - continue;
690 - }
352 + // Fetch all matching transient option names.
353 + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
354 + $transients = $wpdb->get_col(
355 + $wpdb->prepare(
356 + "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
357 + $wpdb->esc_like( $prefix . $transient_ns ) . '%'
358 + )
359 + );
691 360
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 - }
361 + foreach ( $transients as $option_name ) {
362 + $key = str_replace( '_transient_', '', $option_name );
363 + delete_transient( $key );
720 364 }
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 365 }
733 366 }