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