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/Modules/VariationGallery/GalleryFns.php +3 -542 3.4.23.2.6 View file →
@@ -6,8 +6,9 @@
6 6 */
7 7
8 8 namespace RadiusTheme\SB\Modules\VariationGallery;
9 9
10 +use RadiusTheme\SB\Helpers\Cache;
10 11 use RadiusTheme\SB\Helpers\Fns;
11 12 use WC_Product;
12 13
13 14 defined( 'ABSPATH' ) || exit();
@@ -15,252 +16,9 @@
15 16 /**
16 17 * Sticky add-to-cart Functions Class.
17 18 */
18 19 class GalleryFns {
19 -
20 20 /**
21 - * Legacy post meta key holding this module's own variation gallery.
22 - *
23 - * Kept in place forever: it keeps CSV export/import and any third-party reader
24 - * working, and lets a store roll back to an older WooCommerce without data loss.
25 - *
26 - * @var string
27 - */
28 - const LEGACY_GALLERY_META_KEY = 'rtsb_vg_images';
29 -
30 - /**
31 - * Sentinel meta marking a variation whose gallery is owned by the native store.
32 - *
33 - * Set by the migration and by the admin save path. Its presence — not its value —
34 - * is what matters: once stamped, an empty native gallery is an explicit "no
35 - * images" choice, so the legacy meta must never resurrect removed images.
36 - *
37 - * @var string
38 - */
39 - const NATIVE_GALLERY_SENTINEL_META_KEY = '_rtsb_vg_gallery_migrated';
40 -
41 - /**
42 - * First WooCommerce version shipping the native variation gallery for everyone.
43 - *
44 - * Only used as a fallback when WooCommerce's own feature gate cannot be reached.
45 - *
46 - * @var string
47 - */
48 - const NATIVE_GALLERY_WC_VERSION = '11.1.0';
49 -
50 - /**
51 - * WooCommerce's variation gallery package, which owns the feature gate.
52 - *
53 - * @var string
54 - */
55 - const NATIVE_GALLERY_PACKAGE = 'Automattic\WooCommerce\Internal\VariationGallery\Package';
56 -
57 - /**
58 - * Whether the running WooCommerce provides the native variation gallery.
59 - *
60 - * WooCommerce 11.0 ships the feature behind an option/canary gate and 11.1 turns
61 - * it on for everyone, so a bare version comparison would mis-read the 11.0 stores
62 - * that already have it. WooCommerce's own gate is asked first and the version
63 - * comparison is only the fallback for builds where that class is unavailable.
64 - *
65 - * This is the single decision point for the whole module — nothing else should
66 - * test the WooCommerce version for this feature.
67 - *
68 - * @return bool
69 - */
70 - public static function use_native_gallery() {
71 - static $supported = null;
72 -
73 - if ( null === $supported ) {
74 - $package = self::NATIVE_GALLERY_PACKAGE;
75 -
76 - if ( class_exists( $package ) && method_exists( $package, 'is_enabled' ) ) {
77 - $supported = (bool) $package::is_enabled();
78 - } else {
79 - $supported = defined( 'WC_VERSION' ) && version_compare( WC_VERSION, self::NATIVE_GALLERY_WC_VERSION, '>=' );
80 - }
81 - }
82 -
83 - return (bool) apply_filters( 'rtsb/vg/use/native/gallery', $supported );
84 - }
85 -
86 - /**
87 - * Read the native variation gallery IDs straight from storage.
88 - *
89 - * Uses the `edit` context on purpose: the `view` context runs
90 - * `woocommerce_product_variation_get_gallery_image_ids`, which this module filters
91 - * on the frontend to stop WooCommerce rendering its own competing gallery markup.
92 - * Reading raw keeps that suppression from feeding back into our own resolution.
93 - *
94 - * @param \WC_Product_Variation $variation Variation object.
95 - *
96 - * @return array Attachment IDs.
97 - */
98 - public static function get_native_variation_gallery_ids( $variation ) {
99 - if ( ! is_object( $variation ) || ! method_exists( $variation, 'get_gallery_image_ids' ) ) {
100 - return [];
101 - }
102 -
103 - return array_values( array_filter( array_map( 'absint', (array) $variation->get_gallery_image_ids( 'edit' ) ) ) );
104 - }
105 -
106 - /**
107 - * Whether a variation's gallery is already owned by the native store.
108 - *
109 - * @param int $variation_id Variation ID.
110 - *
111 - * @return bool
112 - */
113 - public static function is_native_gallery_owned( $variation_id ) {
114 - return metadata_exists( 'post', absint( $variation_id ), self::NATIVE_GALLERY_SENTINEL_META_KEY );
115 - }
116 -
117 - /**
118 - * Mark a variation's gallery as owned by the native store.
119 - *
120 - * @param int $variation_id Variation ID.
121 - *
122 - * @return void
123 - */
124 - public static function mark_native_gallery_owned( $variation_id ) {
125 - update_post_meta( absint( $variation_id ), self::NATIVE_GALLERY_SENTINEL_META_KEY, 'yes' );
126 - }
127 -
128 - /**
129 - * Read the legacy gallery IDs stored by this module.
130 - *
131 - * @param int $variation_id Variation ID.
132 - *
133 - * @return array Attachment IDs.
134 - */
135 - public static function get_legacy_variation_gallery_ids( $variation_id ) {
136 - $legacy_ids = get_post_meta( absint( $variation_id ), self::LEGACY_GALLERY_META_KEY, true );
137 -
138 - return array_values( array_filter( array_map( 'absint', (array) $legacy_ids ) ) );
139 - }
140 -
141 - /**
142 - * Resolve the ordered gallery attachment IDs for a variation.
143 - *
144 - * With the native gallery available it is authoritative, and the legacy meta is
145 - * only a fallback for variations the migration has not processed yet. Without it
146 - * the legacy meta stays authoritative, exactly as before. In both cases the parent
147 - * product gallery remains the last resort, so a variation with no gallery of its
148 - * own keeps showing the product's thumbnail strip instead of collapsing to a
149 - * single image.
150 - *
151 - * The variation's featured image is not part of this list; the caller prepends it,
152 - * matching WooCommerce's own featured/gallery split.
153 - *
154 - * @param \WC_Product $variation Variation object.
155 - * @param \WC_Product $parent_product Parent product object, or false.
156 - *
157 - * @return array Attachment IDs.
158 - */
159 - protected static function resolve_variation_gallery_image_ids( $variation, $parent_product ) {
160 - $variation_id = absint( $variation->get_id() );
161 -
162 - if ( self::use_native_gallery() ) {
163 - $native_ids = self::get_native_variation_gallery_ids( $variation );
164 -
165 - // Migrated variation: the native gallery is the source of truth and the
166 - // legacy meta is never consulted — it does not need to exist at all.
167 - if ( ! empty( $native_ids ) ) {
168 - return $native_ids;
169 - }
170 -
171 - // Pre-migration variation: keep serving the legacy meta until the batch
172 - // runner has copied it across. Once the sentinel is set, an empty native
173 - // gallery is a deliberate "no images" choice and must be respected.
174 - if ( ! self::is_native_gallery_owned( $variation_id ) ) {
175 - $legacy_ids = self::get_legacy_variation_gallery_ids( $variation_id );
176 -
177 - if ( ! empty( $legacy_ids ) ) {
178 - return $legacy_ids;
179 - }
180 - }
181 - } else {
182 - $legacy_ids = self::get_legacy_variation_gallery_ids( $variation_id );
183 -
184 - if ( ! empty( $legacy_ids ) ) {
185 - return $legacy_ids;
186 - }
187 - }
188 -
189 - if ( $parent_product ) {
190 - return (array) $parent_product->get_gallery_image_ids();
191 - }
192 -
193 - return (array) $variation->get_gallery_image_ids();
194 - }
195 -
196 - /**
197 - * Resolve the saved thumbnail position / gallery style.
198 - *
199 - * Bottom, left and right ship with the free version. The grid layout needs the pro
200 - * templates, so a stored `grid` value falls back to the bottom position whenever
201 - * pro is not active — otherwise a downgraded site would try to render a layout it
202 - * has no template for.
203 - *
204 - * Single decision point: every caller resolves the style through here rather than
205 - * reading the raw option, so the free/pro split cannot drift between the template,
206 - * the template override and the pro slider options.
207 - *
208 - * @return string One of `bottom`, `left`, `right` or `grid`.
209 - */
210 - public static function get_gallery_style() {
211 - $saved = self::get_options( 'gallery_style' );
212 -
213 - if ( ! $saved ) {
214 - $saved = 'bottom';
215 - }
216 -
217 - if ( 'grid' === $saved && ! rtsb()->has_pro() ) {
218 - $saved = 'bottom';
219 - }
220 -
221 - return apply_filters( 'rtsb/vg/gallery/style', $saved );
222 - }
223 -
224 - /**
225 - * Whether any of a variable product's variations carries its own gallery.
226 - *
227 - * The thumbnail wrapper must exist in the DOM when at least one variation has a
228 - * gallery, so JS can populate it on variation change; otherwise it is skipped to
229 - * avoid a blank column. Both stores are consulted so the answer stays correct
230 - * before, during and after the migration.
231 - *
232 - * @param \WC_Product $product Parent product object.
233 - *
234 - * @return bool
235 - */
236 - public static function product_has_variation_gallery( $product ) {
237 - if ( ! ( $product instanceof WC_Product ) || ! $product->is_type( 'variable' ) ) {
238 - return false;
239 - }
240 -
241 - $native = self::use_native_gallery();
242 -
243 - foreach ( $product->get_children() as $child_id ) {
244 - // Native first: a migrated store answers without ever reading the legacy
245 - // meta, which may no longer exist.
246 - if ( $native ) {
247 - $variation = wc_get_product( $child_id );
248 -
249 - if ( $variation && self::get_native_variation_gallery_ids( $variation ) ) {
250 - return true;
251 - }
252 - }
253 -
254 - if ( self::get_legacy_variation_gallery_ids( $child_id ) ) {
255 - return true;
256 - }
257 - }
258 -
259 - return false;
260 - }
261 -
262 - /**
263 21 * @param string $key Default Attribute.
264 22 * @param array|string $default Default Attribute.
265 23 * @return array|string
266 24 */
@@ -320,12 +78,8 @@
320 78 $full_src = wp_get_attachment_image_src( $attachment_id, $full_size );
321 79 $alt_text = trim( wp_strip_all_tags( get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ) ) );
322 80 $alt_text = ( empty( $alt_text ) && ( $product instanceof WC_Product ) ) ? woocommerce_get_alt_from_product_title_and_position( $product->get_title(), $main_image, $image_index ) : $alt_text;
323 81
324 - if ( ! is_array( $full_src ) ) {
325 - $full_src = [ '', '', '' ];
326 - }
327 -
328 82 /**
329 83 * Filters the attributes for the image markup.
330 84 *
331 85 * @since 3.3.2
@@ -420,310 +174,17 @@
420 174 array_unshift( $attachment_ids, $post_thumbnail_id );
421 175 }
422 176 if ( is_array( $attachment_ids ) && ! empty( $attachment_ids ) ) {
423 177 foreach ( $attachment_ids as $i => $image_id ) {
424 - // Skip IDs whose attachment was deleted from the media library so
425 - // the gallery never renders an empty (bordered) slide.
426 - if ( $image_id && wp_attachment_is_image( $image_id ) ) {
178 + if ( $image_id ) {
427 179 $images[ $i ] = self::product_attachment_props( $image_id, $product );
428 180 }
429 181 }
430 182 }
431 183 set_transient( $transient_name, $images, 12 * HOUR_IN_SECONDS );
184 + Cache::set_transient_cache_key( $transient_name );
432 185 return apply_filters( 'rtsb/vg/get/gallery/images', $images, $product_id );
433 186 }
434 - /**
435 - * Build (and cache) the gallery image props for a single variation.
436 - *
437 - * Assembles the image-ID list for one variation only — the per-variation logic
438 - * previously executed for every variation inside the
439 - * `woocommerce_available_variation` filter — so galleries can be fetched
440 - * on-demand over AJAX instead of being built in bulk on page load.
441 - *
442 - * The result is cached in a transient keyed by variation ID and is always a
443 - * sequential array (built with `$images[]`) so it JSON-encodes to a JS array.
444 - *
445 - * @param int $product_id Parent (variable) product ID.
446 - * @param int $variation_id Variation ID whose gallery should be built.
447 - *
448 - * @return array Sequential array of image prop arrays.
449 - */
450 - public static function get_variation_gallery( $product_id, $variation_id ) {
451 - $variation_id = absint( $variation_id );
452 - $product_id = absint( $product_id );
453 -
454 - $transient_name = self::get_transient_name( $variation_id, 'variation-images' );
455 - $cached = $transient_name ? get_transient( $transient_name ) : false;
456 - if ( ! empty( $cached ) && is_array( $cached ) ) {
457 - return apply_filters( 'rtsb/vg/get/variation/gallery', $cached, $product_id, $variation_id );
458 - }
459 -
460 - $variation = wc_get_product( $variation_id );
461 - if ( ! $variation ) {
462 - return [];
463 - }
464 -
465 - $parent_product = $product_id ? wc_get_product( $product_id ) : false;
466 -
467 - $gallery_images = self::resolve_variation_gallery_image_ids( $variation, $parent_product );
468 -
469 - $lead_image_id = absint( $variation->get_image_id() );
470 - if ( empty( $lead_image_id ) && $parent_product ) {
471 - // Variation has no featured image of its own — lead with the parent
472 - // product's featured image (WooCommerce-style image swap).
473 - $lead_image_id = absint( $parent_product->get_image_id() );
474 - }
475 - if ( ! empty( $lead_image_id ) ) {
476 - array_unshift( $gallery_images, $lead_image_id );
477 - }
478 -
479 - // Drop IDs whose attachment was deleted from the media library so the
480 - // variation gallery never renders an empty (bordered) thumbnail/main slide.
481 - $gallery_images = array_filter( array_unique( array_filter( array_map( 'absint', $gallery_images ) ) ), 'wp_attachment_is_image' );
482 - $gallery_images = array_values( $gallery_images );
483 -
484 - $images = [];
485 - foreach ( $gallery_images as $image_id ) {
486 - if ( $image_id ) {
487 - // Always append so the result stays a sequential (JS) array.
488 - $images[] = self::product_attachment_props( $image_id, $variation );
489 - }
490 - }
491 -
492 - if ( $transient_name ) {
493 - set_transient( $transient_name, $images, HOUR_IN_SECONDS * 12 );
494 - }
495 -
496 - return apply_filters( 'rtsb/vg/get/variation/gallery', $images, $product_id, $variation_id );
497 - }
498 -
499 - /**
500 - * Resolve the default variation ID for a variable product.
501 - *
502 - * Returns the variation that WooCommerce's default attributes fully resolve to,
503 - * so the matching gallery can be rendered server-side on initial page load.
504 - *
505 - * @param \WC_Product $product Variable product object.
506 - *
507 - * @return int Variation ID, or 0 when no single default variation resolves.
508 - */
509 - public static function get_default_variation_id( $product ) {
510 - if ( ! ( $product instanceof WC_Product ) || ! $product->is_type( 'variable' ) ) {
511 - return 0;
512 - }
513 -
514 - $default_attributes = $product->get_default_attributes();
515 - if ( empty( $default_attributes ) ) {
516 - return 0;
517 - }
518 -
519 - $match_attributes = [];
520 - foreach ( $default_attributes as $key => $value ) {
521 - $match_attributes[ 'attribute_' . $key ] = $value;
522 - }
523 -
524 - $data_store = \WC_Data_Store::load( 'product' );
525 - $variation_id = $data_store->find_matching_product_variation( $product, $match_attributes );
526 -
527 - return absint( $variation_id );
528 - }
529 -
530 - /**
531 - * Resolve a variation ID from the URL attribute params.
532 - *
533 - * Reads the same `attribute_*` request params WooCommerce uses to pre-select
534 - * the variation form (e.g. ?attribute_pa_color=green&attribute_pa_size=medium)
535 - * so the matching variation gallery can be painted server-side on initial load.
536 - * Only resolves when every variation attribute is present, otherwise returns 0.
537 - *
538 - * @param \WC_Product $product Variable product object.
539 - *
540 - * @return int Variation ID, or 0 when the params do not uniquely resolve one.
541 - */
542 - public static function get_requested_variation_id( $product ) {
543 - if ( ! ( $product instanceof WC_Product ) || ! $product->is_type( 'variable' ) ) {
544 - return 0;
545 - }
546 -
547 - // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only gallery paint using the same attribute params WooCommerce itself reads to pre-select variations.
548 - $request = wp_unslash( $_REQUEST );
549 -
550 - $match_attributes = [];
551 - foreach ( $product->get_variation_attributes() as $attribute_name => $options ) {
552 - $key = 'attribute_' . sanitize_title( $attribute_name );
553 - if ( ! isset( $request[ $key ] ) ) {
554 - // A required attribute is missing, so a single variation cannot resolve.
555 - return 0;
556 - }
557 - $value = wc_clean( $request[ $key ] );
558 - if ( '' === $value ) {
559 - return 0;
560 - }
561 - $match_attributes[ $key ] = $value;
562 - }
563 -
564 - if ( empty( $match_attributes ) ) {
565 - return 0;
566 - }
567 -
568 - $data_store = \WC_Data_Store::load( 'product' );
569 - $variation_id = $data_store->find_matching_product_variation( $product, $match_attributes );
570 -
571 - return absint( $variation_id );
572 - }
573 -
574 - /**
575 - * Get the attachment IDs to render for a product's initial gallery paint.
576 - *
577 - * When a default variation resolves, returns that variation's gallery image IDs
578 - * (and warms its transient as a side effect of get_variation_gallery()). Otherwise
579 - * falls back to the parent product's featured + gallery images.
580 - *
581 - * @param \WC_Product $product Product object.
582 - *
583 - * @return array {
584 - * @type int[] $image_ids Sequential attachment IDs to render.
585 - * @type \WC_Product $context Product/variation object to use as render context.
586 - * }
587 - */
588 - public static function get_initial_gallery_render( $product ) {
589 - $context = $product;
590 - $image_ids = [];
591 - $variation_id = 0;
592 -
593 - if ( $product instanceof WC_Product ) {
594 - // Prefer the variation requested via URL attribute params (the same
595 - // params WooCommerce reads to pre-select the variation form) so the
596 - // correct gallery is painted server-side; fall back to the product's
597 - // default variation otherwise.
598 - $selected_variation_id = self::get_requested_variation_id( $product );
599 - if ( ! $selected_variation_id ) {
600 - $selected_variation_id = self::get_default_variation_id( $product );
601 - }
602 - if ( $selected_variation_id ) {
603 - $variation_props = self::get_variation_gallery( $product->get_id(), $selected_variation_id );
604 - foreach ( (array) $variation_props as $prop ) {
605 - if ( ! empty( $prop['image_id'] ) ) {
606 - $image_ids[] = absint( $prop['image_id'] );
607 - }
608 - }
609 - if ( ! empty( $image_ids ) ) {
610 - $variation = wc_get_product( $selected_variation_id );
611 - if ( $variation ) {
612 - $context = $variation;
613 - }
614 - // The server has painted this variation's gallery, so JS can
615 - // skip its redundant initial AJAX/re-render for it.
616 - $variation_id = $selected_variation_id;
617 - }
618 - }
619 - }
620 -
621 - if ( empty( $image_ids ) && ( $product instanceof WC_Product ) ) {
622 - $post_thumbnail_id = $product->get_image_id();
623 - $attachment_ids = $product->get_gallery_image_ids();
624 - if ( $post_thumbnail_id ) {
625 - $image_ids[] = absint( $post_thumbnail_id );
626 - }
627 - if ( is_array( $attachment_ids ) && count( $attachment_ids ) ) {
628 - foreach ( $attachment_ids as $attachment_id ) {
629 - $image_ids[] = absint( $attachment_id );
630 - }
631 - }
632 - }
633 -
634 - // Drop IDs whose attachment was deleted from the media library so the
635 - // gallery never renders an empty (bordered) thumbnail/main slide.
636 - $image_ids = array_filter( array_unique( array_filter( $image_ids ) ), 'wp_attachment_is_image' );
637 - $image_ids = array_values( $image_ids );
638 -
639 - return [
640 - 'image_ids' => $image_ids,
641 - 'context' => $context,
642 - 'variation_id' => $variation_id,
643 - ];
644 - }
645 -
646 - /**
647 - * Flush all gallery transients (default + every variation) for a product.
648 - *
649 - * Used when a product or one of its variations is saved so stale gallery
650 - * props are not served from cache.
651 - *
652 - * @param int $product_id Parent product ID.
653 - *
654 - * @return void
655 - */
656 - public static function flush_product_gallery_transients( $product_id ) {
657 - $product_id = absint( $product_id );
658 - if ( ! $product_id ) {
659 - return;
660 - }
661 -
662 - self::delete_transients( $product_id, 'default-images' );
663 -
664 - $product = wc_get_product( $product_id );
665 - if ( ! $product || ! $product->is_type( 'variable' ) ) {
666 - return;
667 - }
668 -
669 - foreach ( $product->get_children() as $variation_id ) {
670 - self::delete_transients( $variation_id, 'variation-images' );
671 - }
672 - }
673 -
674 - /**
675 - * Flush variation gallery transients that reference a given attachment.
676 - *
677 - * Invoked when an attachment is deleted so variations whose galleries include
678 - * the removed image rebuild their props on next request.
679 - *
680 - * @param int $attachment_id Deleted attachment ID.
681 - *
682 - * @return void
683 - */
684 - public static function flush_attachment_gallery_transients( $attachment_id ) {
685 - global $wpdb;
686 -
687 - $attachment_id = absint( $attachment_id );
688 - if ( ! $attachment_id ) {
689 - return;
690 - }
691 -
692 - // Flush the owning product (featured/parent) galleries when determinable.
693 - $parent_id = wp_get_post_parent_id( $attachment_id );
694 - if ( $parent_id ) {
695 - self::flush_product_gallery_transients( $parent_id );
696 - }
697 -
698 - // Flush any variation whose gallery references this attachment. Both stores are
699 - // scanned: the legacy meta holds a serialised array, while WooCommerce's native
700 - // variation gallery holds a comma-separated string under `_product_image_gallery`.
701 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
702 - $rows = $wpdb->get_results(
703 - $wpdb->prepare(
704 - "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key IN ( %s, %s )",
705 - self::LEGACY_GALLERY_META_KEY,
706 - '_product_image_gallery'
707 - )
708 - );
709 -
710 - if ( empty( $rows ) ) {
711 - return;
712 - }
713 -
714 - foreach ( $rows as $row ) {
715 - $image_ids = maybe_unserialize( $row->meta_value );
716 - if ( ! is_array( $image_ids ) ) {
717 - $image_ids = explode( ',', (string) $image_ids );
718 - }
719 - $image_ids = array_map( 'absint', $image_ids );
720 - if ( in_array( $attachment_id, $image_ids, true ) ) {
721 - self::delete_transients( $row->post_id, 'variation-images' );
722 - }
723 - }
724 - }
725 -
726 187 /**
727 188 * Helper: Get all images transient name for specific variation/product
728 189 *
729 190 * @param int $id unique id.