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/GalleryFrontEnd.php +29 -390 3.4.23.2.6 View file →
@@ -7,8 +7,9 @@
7 7
8 8 namespace RadiusTheme\SB\Modules\VariationGallery;
9 9
10 10 use RadiusTheme\SB\Helpers\BuilderFns;
11 +use RadiusTheme\SB\Helpers\Cache;
11 12 use RadiusTheme\SB\Helpers\Fns;
12 13 use RadiusTheme\SB\Traits\SingletonTrait;
13 14
14 15 defined( 'ABSPATH' ) || exit();
@@ -28,33 +29,12 @@
28 29 */
29 30 private function __construct() {
30 31 add_filter( 'woocommerce_locate_template', [ $this, 'override_templates' ], 10, 2 );
31 32 add_action( 'wp_footer', [ $this, 'slider_and_thumbnail_template_js' ] );
32 -
33 - // Side (left/right) thumbnail layouts are free. Pro re-filters both of these at
34 - // its own priority and wins where the two overlap, so pro behaviour is unchanged.
35 - add_filter( 'rtsb/vg/slider/options', [ $this, 'side_thumbnail_slider_options' ] );
36 - add_filter( 'rtsb/vg/thumbnails/slider/options', [ $this, 'side_thumbnails_slider_options' ], 10, 2 );
37 33 add_filter( 'woocommerce_available_variation', [ $this, 'available_variation_gallery' ], 90, 3 );
38 - add_filter( 'woocommerce_product_variation_get_gallery_image_ids', [ $this, 'suppress_native_variation_gallery' ], 20, 2 );
39 34
40 - // Track the variation form so its embedded "default gallery" snapshot can be
41 - // skipped. `wc_get_template` (not `woocommerce_locate_template`) is what the
42 - // stub swaps on, because WooCommerce caches the located path and skips the
43 - // locate filter on repeat renders within the same request.
44 - add_action( 'woocommerce_before_template_part', [ $this, 'mark_variation_form_open' ] );
45 - add_action( 'woocommerce_after_template_part', [ $this, 'mark_variation_form_closed' ] );
46 - add_filter( 'wc_get_template', [ $this, 'skip_native_gallery_snapshot' ], 60, 2 );
47 -
48 35 add_action( 'wp_ajax_rtsb_vg_get_default_gallery_images', [ $this, 'get_default_gallery_images' ] );
49 36 add_action( 'wp_ajax_nopriv_rtsb_vg_get_default_gallery_images', [ $this, 'get_default_gallery_images' ] );
50 -
51 - // On-demand single-variation gallery. Nonce-less / read-only by design,
52 - // mirroring WooCommerce core's own `woocommerce_get_variation` AJAX: a
53 - // required nonce breaks on full-page-cached pages because the embedded
54 - // nonce goes stale, and the payload is public, read-only product images.
55 - add_action( 'wp_ajax_rtsb_get_variation_gallery', [ $this, 'get_variation_gallery' ] );
56 - add_action( 'wp_ajax_nopriv_rtsb_get_variation_gallery', [ $this, 'get_variation_gallery' ] );
57 37 }
58 38 /**
59 39 * @param array $available_variation Available variation.
60 40 * @param object $variationProductObject object.
@@ -62,331 +42,36 @@
62 42 *
63 43 * @return array
64 44 */
65 45 public function available_variation_gallery( $available_variation, $variationProductObject, $variation ) {
66 - $variation_id = absint( $variation->get_id() );
67 -
68 - if ( $this->is_single_product_context() ) {
69 - // On single product pages the Variation Gallery replaces the WooCommerce
70 - // product gallery (see override_templates()) and controls the image swap
71 - // itself.
72 - //
73 - // We strip WooCommerce core's own variation `image` data so its variation
74 - // form does not ALSO try to swap `.wp-post-image`. Otherwise the two handlers
75 - // fight over the same element — causing flicker, stuck navigation, and the
76 - // gallery getting stuck on the featured Product Image (WC core's fallback
77 - // when a variation has no image of its own).
78 - if ( apply_filters( 'rtsb/vg/disable_wc_variation_image', true, $available_variation, $variation ) ) {
79 - unset( $available_variation['image'], $available_variation['image_id'] );
80 - }
81 -
82 - /*
83 - * WooCommerce 11.1+ also ships gallery markup in `gallery_images_html` and
84 - * swaps it in from `add-to-cart-variation.js`, replacing the whole
85 - * `.woocommerce-product-gallery` element. Our gallery owns that DOM, so the
86 - * payload is dropped to keep core's swap from competing with ours — and to
87 - * keep a duplicate copy of the gallery out of `data-product_variations`.
88 - *
89 - * suppress_native_variation_gallery() normally stops this being built at
90 - * all; blanking it here is the belt-and-braces for any request where the
91 - * gallery ids were re-added by a third party after that filter ran.
92 - */
93 - if ( GalleryFns::use_native_gallery() && isset( $available_variation['gallery_images_html'] ) ) {
94 - $available_variation['gallery_images_html'] = '';
95 - }
96 -
97 - // Inline the gallery for small products so variation swaps are instant with no
98 - // per-click AJAX (the frontend renders variation_gallery_images directly). For
99 - // large products we omit it and fall back to the lazy AJAX path
100 - // (get_variation_gallery()) to keep the page lean. WooCommerce itself only
101 - // embeds variations inline below woocommerce_ajax_variation_threshold, so
102 - // inlining above the threshold would bloat the page with no benefit.
103 - $children = ( $variationProductObject instanceof \WC_Product ) ? $variationProductObject->get_children() : [];
104 - $threshold = absint( apply_filters( 'rtsb/vg/inline_gallery_threshold', 20 ) );
105 -
106 - if ( $threshold && count( $children ) <= $threshold ) {
107 - $available_variation['variation_gallery_images'] = GalleryFns::get_variation_gallery( $variation->get_parent_id(), $variation_id );
108 - }
109 -
110 - return apply_filters( 'rtsb/vg/available/variation/gallery', $available_variation, $variation, $variation_id );
111 - }
112 -
113 - // Other contexts (e.g. the archive/shop variation image swap, handled by the
114 - // Variation Swatches showcase which still needs WC's `image`) consume the
115 - // inline data, so expose an already-cached gallery if one exists. This is a
116 - // cheap get_transient() — no prop building.
46 + $variation_id = absint( $variation->get_id() );
117 47 $transient_name = GalleryFns::get_transient_name( $variation_id, 'variation-images' );
118 - $images = $transient_name ? get_transient( $transient_name ) : false;
48 + $images = get_transient( $transient_name );
119 49 if ( ! empty( $images ) && is_array( $images ) ) {
120 - $available_variation['variation_gallery_images'] = array_values( $images );
50 + $available_variation['variation_gallery_images'] = $images;
51 + return apply_filters( 'rtsb/vg/available/variation/gallery', $available_variation, $variation, $variation_id );
121 52 }
122 -
123 - return apply_filters( 'rtsb/vg/available/variation/gallery', $available_variation, $variation, $variation_id );
124 - }
125 -
126 - /**
127 - * Turn the main slider into a thumbnail-linked Swiper for side layouts.
128 - *
129 - * The bottom layout is deliberately left alone: its thumbnails are plain stacked
130 - * items (`thumbsSelectorNoSlider`), and switching it to a Swiper would change how
131 - * every existing free site renders. Only left/right — which need a vertical,
132 - * linked thumbnail strip — are adjusted here.
133 - *
134 - * @param array $slider_options Main slider options.
135 - *
136 - * @return array
137 - */
138 - public function side_thumbnail_slider_options( $slider_options ) {
139 - if ( ! $this->is_side_thumbnail_layout( GalleryFns::get_gallery_style() ) ) {
140 - return $slider_options;
53 + $has_variation_gallery_images = (bool) get_post_meta( $variation_id, 'rtsb_vg_images', true );
54 + if ( $has_variation_gallery_images ) {
55 + $gallery_images = (array) get_post_meta( $variation_id, 'rtsb_vg_images', true );
56 + } else {
57 + $gallery_images = $variationProductObject->get_gallery_image_ids();
141 58 }
142 -
143 - // Side layouts always track the active image's height: the two columns sit
144 - // next to each other, so sizing to the tallest slide would leave a blank gap
145 - // under a short image.
146 - $slider_options['autoHeight'] = true;
147 - $slider_options['observer'] = true;
148 - $slider_options['observeParents'] = true;
149 - $slider_options['thumbsSelector'] = '.rtsb-vg-thumb-slider';
150 -
151 - unset( $slider_options['thumbsSelectorNoSlider'] );
152 -
153 - return $slider_options;
154 - }
155 -
156 - /**
157 - * Supply the vertical thumbnail Swiper options for side layouts.
158 - *
159 - * @param array $options Thumbnail slider options.
160 - * @param array $args Render args (thumbnailsPosition, randDom).
161 - *
162 - * @return array
163 - */
164 - public function side_thumbnails_slider_options( $options, $args ) {
165 - $position = $args['thumbnailsPosition'] ?? 'bottom';
166 -
167 - if ( ! $this->is_side_thumbnail_layout( $position ) ) {
168 - return $options;
59 + $variation_image_id = absint( $variation->get_image_id() );
60 + if ( ! empty( $variation_image_id ) ) {
61 + array_unshift( $gallery_images, $variation_image_id );
169 62 }
170 -
171 - $col = GalleryFns::get_options( 'thumbnails_columns' );
172 - $gap = GalleryFns::get_options( 'thumbnails_gap' );
173 -
174 - return [
175 - 'slidesPerView' => absint( $col ?: 4 ),
176 - 'spaceBetween' => ( null === $gap || '' === $gap ) ? 10 : absint( $gap ),
177 - 'direction' => 'vertical',
178 - 'speed' => 500,
179 - 'loop' => false,
180 - 'navigation' => [
181 - 'nextEl' => '.swiper-gallery-next.rtsb-random-id-' . esc_attr( $args['randDom'] ?? '' ),
182 - 'prevEl' => '.swiper-gallery-prev.rtsb-random-id-' . esc_attr( $args['randDom'] ?? '' ),
183 - ],
184 - ] + $options;
185 - }
186 -
187 - /**
188 - * Whether a gallery style places the thumbnails beside the main image.
189 - *
190 - * @param string $style Gallery style / thumbnail position.
191 - *
192 - * @return bool
193 - */
194 - private function is_side_thumbnail_layout( $style ) {
195 - return in_array( $style, [ 'left', 'right' ], true );
196 - }
197 -
198 - /**
199 - * Whether WooCommerce is currently rendering the variable add-to-cart template.
200 - *
201 - * @var bool
202 - */
203 - private $rendering_variation_form = false;
204 -
205 - /**
206 - * Whether this module's gallery template replaced WooCommerce's for this request.
207 - *
208 - * Used to decide when it is safe to stop WooCommerce building its own variation
209 - * gallery output: only once our template is demonstrably the one rendering the
210 - * product images. Block themes and the WooCommerce Product Gallery block never go
211 - * through `single-product/product-image.php`, so they must keep reading the native
212 - * gallery untouched even though `is_product()` is true there.
213 - *
214 - * @var bool
215 - */
216 - private $gallery_template_rendered = false;
217 -
218 - /**
219 - * Note that the variable add-to-cart template has started rendering.
220 - *
221 - * @param string $template_name Template being rendered.
222 - *
223 - * @return void
224 - */
225 - public function mark_variation_form_open( $template_name ) {
226 - if ( 'single-product/add-to-cart/variable.php' === $template_name ) {
227 - $this->rendering_variation_form = true;
228 - }
229 - }
230 -
231 - /**
232 - * Note that the variable add-to-cart template has finished rendering.
233 - *
234 - * @param string $template_name Template that finished rendering.
235 - *
236 - * @return void
237 - */
238 - public function mark_variation_form_closed( $template_name ) {
239 - if ( 'single-product/add-to-cart/variable.php' === $template_name ) {
240 - $this->rendering_variation_form = false;
241 - }
242 - }
243 -
244 - /**
245 - * Skip the gallery snapshot WooCommerce embeds inside the variation form.
246 - *
247 - * `single-product/add-to-cart/variable.php` renders the whole product gallery a
248 - * second time into a hidden `<template>`, so core's variation script can restore it
249 - * after swapping in per-variation markup. That swap never happens here — the
250 - * payload driving it is dropped in available_variation_gallery() — so the snapshot
251 - * is dead weight: it would duplicate the entire slider on every variable product
252 - * page and fire the gallery's render hooks a second time.
253 - *
254 - * Scoped to renders that happen *inside* the variation form, so a genuine second
255 - * gallery elsewhere on the page is untouched.
256 - *
257 - * @param string $template Resolved template path.
258 - * @param string $template_name Template name being loaded.
259 - *
260 - * @return string
261 - */
262 - public function skip_native_gallery_snapshot( $template, $template_name ) {
263 - if ( 'single-product/product-image.php' !== $template_name ) {
264 - return $template;
265 - }
266 -
267 - // Re-assert ownership here as well as in override_templates(): WooCommerce
268 - // caches the located path, so on a store with a persistent object cache the
269 - // locate filter can be skipped entirely while this one always runs. The
270 - // snapshot render is excluded, otherwise it would claim ownership on behalf of
271 - // a gallery that has not actually been rendered.
272 - if ( ! $this->rendering_variation_form && false !== strpos( $template, 'variation-gallery' ) ) {
273 - $this->gallery_template_rendered = true;
274 - }
275 -
276 - if ( ! $this->rendering_variation_form ) {
277 - return $template;
278 - }
279 -
280 - if ( ! GalleryFns::use_native_gallery() || ! $this->gallery_template_rendered || ! $this->is_single_product_context() ) {
281 - return $template;
282 - }
283 -
284 - if ( ! apply_filters( 'rtsb/vg/skip/native/gallery/snapshot', true ) ) {
285 - return $template;
286 - }
287 -
288 - $stub = Fns::locate_template( 'variation-gallery/vg-noop' );
289 -
290 - return file_exists( $stub ) ? $stub : $template;
291 - }
292 -
293 - /**
294 - * Stop WooCommerce building its own variation gallery markup on the frontend.
295 - *
296 - * `WC_Product_Variable::get_available_variation()` renders
297 - * `single-product/product-image.php` once per variation whenever the native gallery
298 - * holds images — and on a single product page that template is ours, so after
299 - * migration every variation would re-render the full slider into
300 - * `data-product_variations`. Returning an empty list skips that render entirely;
301 - * the gallery data our script consumes is supplied separately through
302 - * `variation_gallery_images` and the on-demand AJAX endpoint.
303 - *
304 - * Only the `view` context reaches this filter, so
305 - * `GalleryFns::get_native_variation_gallery_ids()` (which reads in `edit` context)
306 - * and every admin/REST/CSV reader still see the stored value.
307 - *
308 - * @param array $gallery_image_ids Native gallery image IDs.
309 - * @param \WC_Product $variation Variation object.
310 - *
311 - * @return array
312 - */
313 - public function suppress_native_variation_gallery( $gallery_image_ids, $variation ) {
314 - if ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
315 - return $gallery_image_ids;
316 - }
317 -
318 - // `gallery_template_rendered` is the load-bearing condition, not just a
319 - // shortcut: block themes and the WooCommerce Product Gallery block read this
320 - // same prop on a single product page (where is_single_product_context() is
321 - // true) but never render our template, so suppressing there would empty their
322 - // galleries instead of ours.
323 - if ( ! GalleryFns::use_native_gallery() || ! $this->gallery_template_rendered || ! $this->is_single_product_context() ) {
324 - return $gallery_image_ids;
325 - }
326 -
327 - if ( ! apply_filters( 'rtsb/vg/suppress/native/variation/gallery', true, $variation ) ) {
328 - return $gallery_image_ids;
329 - }
330 -
331 - return [];
332 - }
333 -
334 - /**
335 - * Whether the current request is rendering a single product page (where this
336 - * module owns the gallery), as opposed to an archive/shop context.
337 - *
338 - * On page render `is_product()` / BuilderFns::is_product() are reliable. During
339 - * WooCommerce's `get_variation` AJAX (used when the variation threshold is low,
340 - * e.g. with Variation Swatches active) those conditionals are false, so we fall
341 - * back to the referring URL to tell single-product requests apart from
342 - * archive/shop requests. Result is cached for the duration of the request.
343 - *
344 - * @return bool
345 - */
346 - private function is_single_product_context() {
347 - static $is_single = null;
348 -
349 - if ( null !== $is_single ) {
350 - return $is_single;
351 - }
352 -
353 - if ( is_product() || BuilderFns::is_product() ) {
354 - $is_single = true;
355 -
356 - return $is_single;
357 - }
358 -
359 - $is_single = false;
360 -
361 - if ( wp_doing_ajax() ) {
362 - $referer = wp_get_referer();
363 - if ( $referer ) {
364 - $referer_id = url_to_postid( $referer );
365 - if ( $referer_id ) {
366 - $referer_type = get_post_type( $referer_id );
367 -
368 - if ( 'product' === $referer_type ) {
369 - $is_single = true;
370 - } elseif ( BuilderFns::$post_type_tb === $referer_type && 'product' === BuilderFns::builder_type( $referer_id ) ) {
371 - /*
372 - * A ShopBuilder single-product template rendered on its own URL
373 - * (the template-builder preview). The referring post is a
374 - * `rtsb_builder` template rather than a product, so the check
375 - * above misses it — but that page renders this module's gallery
376 - * exactly like a product page does.
377 - *
378 - * Without this, WooCommerce's `get_variation` AJAX keeps its own
379 - * `gallery_images_html`, and `wc_variations_image_update()`
380 - * replaceWith()s our entire gallery on every variation change.
381 - */
382 - $is_single = true;
383 - }
384 - }
63 + $gallery_images = array_values( array_unique( $gallery_images ) );
64 + $images = [];
65 + foreach ( $gallery_images as $i => $image_id ) {
66 + if ( $image_id ) {
67 + $images[ $i ] = GalleryFns::product_attachment_props( $image_id, $variation );
385 68 }
386 69 }
387 -
388 - return $is_single;
70 + set_transient( $transient_name, $images, HOUR_IN_SECONDS * 12 );
71 + Cache::set_transient_cache_key( $transient_name );
72 + $available_variation['variation_gallery_images'] = $images;
73 + return apply_filters( 'rtsb/vg/available/variation/gallery', $available_variation, $variation, $variation_id );
389 74 }
390 75 /**
391 76 * @return void
392 77 */
@@ -413,56 +98,12 @@
413 98 $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
414 99 $images = GalleryFns::get_gallery_images_and_props( $product_id );
415 100 wp_send_json_success( $images );
416 101 }
417 -
418 102 /**
419 - * AJAX: Return the gallery image props for a single selected variation.
420 - *
421 - * Read-only / nonce-less by design, mirroring WooCommerce core's own
422 - * `woocommerce_get_variation` endpoint. A required nonce would break on
423 - * full-page-cached pages (stale embedded nonce); the response is public,
424 - * read-only product image data, so there is no CSRF concern.
425 - *
426 - * @return void
427 - */
428 - public function get_variation_gallery() {
429 - // phpcs:ignore WordPress.Security.NonceVerification.Missing
430 - $variation_id = isset( $_POST['variation_id'] ) ? absint( wp_unslash( $_POST['variation_id'] ) ) : 0;
431 - // phpcs:ignore WordPress.Security.NonceVerification.Missing
432 - $product_id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
433 -
434 - if ( ! $variation_id ) {
435 - wp_send_json_error( esc_html__( 'Invalid variation ID.', 'shopbuilder' ) );
436 - }
437 -
438 - $variation = wc_get_product( $variation_id );
439 -
440 - if ( ! $variation || 'variation' !== $variation->get_type() ) {
441 - wp_send_json_error( esc_html__( 'Variation not found.', 'shopbuilder' ) );
442 - }
443 -
444 - if ( ! $product_id ) {
445 - $product_id = absint( $variation->get_parent_id() );
446 - }
447 -
448 - $product = $product_id ? wc_get_product( $product_id ) : false;
449 -
450 - if ( ! $product || ! $this->can_user_view_product( $product ) ) {
451 - wp_send_json_error( esc_html__( 'You do not have permission to view this product.', 'shopbuilder' ) );
452 - }
453 -
454 - $images = GalleryFns::get_variation_gallery( $product_id, $variation_id );
455 - wp_send_json_success( $images );
456 - }
457 -
458 - /**
459 103 * Determines if the current user has permission to view a product.
460 104 *
461 - * Guests may only access products with a `publish` status. Logged-in users
462 - * are checked against the `read_post` meta capability, which resolves to the
463 - * correct primitive capability for the product's status (published, private,
464 - * draft, etc.) and ownership, preventing exposure of unpublished products.
105 + * Checks product status and visibility settings against user capabilities.
465 106 *
466 107 * @param \WC_Product $product The product to check access for.
467 108 * @return bool True if user can view the product, false otherwise.
468 109 */
@@ -467,14 +108,16 @@
467 108 * @return bool True if user can view the product, false otherwise.
468 109 */
469 110 private function can_user_view_product( $product ) {
470 111 $product_id = $product->get_id();
112 + $status = get_post_status( $product->get_id() );
113 + $visibility = $product->get_catalog_visibility();
471 114
472 - if ( ! is_user_logged_in() ) {
473 - return 'publish' === get_post_status( $product_id );
115 + if ( 'publish' === $status && 'visible' === $visibility ) {
116 + return true;
474 117 }
475 118
476 - return current_user_can( 'read_post', $product_id );
119 + return current_user_can( 'edit_post', $product_id );
477 120 }
478 121 /**
479 122 * @param string $template template.
480 123 * @param string $template_name template name.
@@ -482,17 +125,13 @@
482 125 */
483 126 public function override_templates( $template, $template_name ) {
484 127 // List of templates you want to override.
485 128 if ( 'single-product/product-image.php' === $template_name ) {
486 - $galleryStyle = GalleryFns::get_gallery_style();
129 + $galleryStyle = GalleryFns::get_options( 'gallery_style' );
487 130 $galleryStyle = apply_filters( 'rtsb/vg/thumbnails/position', $galleryStyle );
488 131 $temp = rtsb()->has_pro() && 'grid' === $galleryStyle ? 'variation-gallery/vg-grid-view' : 'variation-gallery/product-image';
489 132 $custom_template = Fns::locate_template( $temp );
490 133 if ( file_exists( $custom_template ) ) {
491 - // Record that our gallery — not WooCommerce's — is what this request
492 - // renders, so the native-output suppression below can safely engage.
493 - $this->gallery_template_rendered = true;
494 -
495 134 return $custom_template;
496 135 }
497 136 }
498 137 return $template;