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
shopbuilder / app / Modules / VariationGallery / GalleryAdmin.php

GalleryAdmin.php in ShopBuilder – WooCommerce Builder For Elementor 3.4.2, at app/Modules/VariationGallery/GalleryAdmin.php

362 lines 13.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Main FilterHooks class.
4 *
5 * @package RadiusTheme\SB
6 */
7
8 namespace RadiusTheme\SB\Modules\VariationGallery;
9
10 use RadiusTheme\SB\Helpers\BuilderFns;
11 use RadiusTheme\SB\Helpers\Cache;
12 use RadiusTheme\SB\Helpers\Fns;
13 use RadiusTheme\SB\Traits\SingletonTrait;
14
15 defined( 'ABSPATH' ) || exit();
16
17 /**
18 * Main FilterHooks class.
19 */
20 class GalleryAdmin {
21
22 /**
23 * Singleton Trait.
24 */
25 use SingletonTrait;
26
27 /**
28 * Module Class Constructor.
29 */
30 private function __construct() {
31 add_action( 'admin_footer', [ $this, 'admin_template_js' ] );
32 add_action( 'woocommerce_product_after_variable_attributes', [ $this, 'gallery_admin_html' ], 10, 3 );
33 add_action( 'woocommerce_save_product_variation', [ $this, 'save_variation_gallery' ] );
34 add_action( 'add_meta_boxes', [ $this, 'add_metabox' ] );
35 add_action( 'save_post', [ $this, 'save_metabox' ] );
36
37 // Late enough that WooCommerce's variation gallery package has registered its
38 // own field, so the duplicate control can be taken back off.
39 add_action( 'admin_init', [ $this, 'remove_native_variation_gallery_field' ], 20 );
40
41 add_filter( 'woocommerce_product_export_meta_value', [ $this, 'product_export_meta_value' ], 15, 3 );
42
43 // Invalidate cached variation galleries when product data or images change.
44 add_action( 'woocommerce_update_product', [ $this, 'flush_product_gallery_cache' ] );
45 add_action( 'woocommerce_save_product_variation', [ $this, 'flush_variation_parent_cache' ] );
46 add_action( 'delete_attachment', [ $this, 'flush_attachment_gallery_cache' ] );
47 }
48
49 /**
50 * Flush all cached gallery transients for a saved product.
51 *
52 * @param int $product_id Product ID.
53 *
54 * @return void
55 */
56 public function flush_product_gallery_cache( $product_id ) {
57 GalleryFns::flush_product_gallery_transients( $product_id );
58 }
59
60 /**
61 * Flush the parent product default-gallery transient when a variation is saved.
62 *
63 * The variation's own transient is already cleared in save_variation_gallery();
64 * this also clears the parent product default-images cache.
65 *
66 * @param int $variation_id Variation ID.
67 *
68 * @return void
69 */
70 public function flush_variation_parent_cache( $variation_id ) {
71 $parent_id = wp_get_post_parent_id( $variation_id );
72 if ( $parent_id ) {
73 GalleryFns::delete_transients( $parent_id, 'default-images' );
74 }
75 }
76
77 /**
78 * Flush cached variation galleries referencing a deleted attachment.
79 *
80 * @param int $attachment_id Attachment ID being deleted.
81 *
82 * @return void
83 */
84 public function flush_attachment_gallery_cache( $attachment_id ) {
85 GalleryFns::flush_attachment_gallery_transients( $attachment_id );
86 }
87 /**
88 * Adds a custom meta box to the WooCommerce product edit page.
89 */
90 public function add_metabox() {
91 add_meta_box(
92 'custom_checkbox_metabox', // Metabox ID.
93 __( 'Variation gallery', 'shopbuilder' ), // Title.
94 [ $this, 'metabox_callback' ], // Callback function.
95 'product', // Post type (WooCommerce product).
96 'side', // Position.
97 'high' // Priority.
98 );
99 }
100 /**
101 * Callback function to display the checkbox in the meta box.
102 *
103 * @param WP_Post $post The post object.
104 */
105 public function metabox_callback( $post ) {
106 $value = get_post_meta( $post->ID, '_rtsb_vg_disable_valiation_gallery', true );
107 ?>
108 <p>
109 <label for="custom_checkbox">
110 <input type="checkbox" id="rtsb_vg_disable_valiation_gallery" name="rtsb_vg_disable_valiation_gallery" value="yes" <?php checked( $value, 'yes' ); ?> />
111 <?php esc_html_e( 'Disable Variation Gallery', 'shopbuilder' ); ?>
112 </label><hr/>
113 <span><?php esc_html_e( 'Disable variation gallery for this product', 'shopbuilder' ); ?> </span>
114 </p>
115 <?php
116 }
117
118 /**
119 * Saves the checkbox value when the product is updated.
120 *
121 * @param int $post_id The ID of the product being saved.
122 */
123 public function save_metabox( $post_id ) {
124 // Verify nonce for security.
125 if ( empty( $_POST['woocommerce_meta_nonce'] ) || ! wp_verify_nonce( wp_unslash( $_POST['woocommerce_meta_nonce'] ), 'woocommerce_save_data' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
126 return;
127 }
128 // Prevent autosave from overwriting.
129 if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
130 return;
131 }
132 // Check if the user has permission to edit the product.
133 if ( ! current_user_can( 'edit_product', $post_id ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown
134 return;
135 }
136 // Save the checkbox value as 'yes' or 'no'.
137 $value = isset( $_POST['rtsb_vg_disable_valiation_gallery'] ) ? 'yes' : 'no';
138 update_post_meta( $post_id, '_rtsb_vg_disable_valiation_gallery', $value );
139 GalleryFns::delete_transients( $post_id, 'default-images' );
140 }
141 /**
142 * Saves variation gallery image IDs from the variation admin panel.
143 *
144 * @param int $variation_id The ID of the product variation.
145 *
146 * @return void
147 */
148 public function save_variation_gallery( $variation_id ) {
149 check_ajax_referer( 'save-variations', 'security' );
150 // Check permissions again and make sure we have what we need.
151 if ( ! current_user_can( 'edit_products' ) || empty( $_POST ) || empty( $_POST['product_id'] ) ) { // phpcs:ignore WordPress.WP.Capabilities.Unknown
152 wp_die( -1 );
153 }
154
155 $rtsb_vg_ids = [];
156
157 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified by check_ajax_referer() above.
158 if ( isset( $_POST['rtsb_vg'][ $variation_id ] ) && is_array( $_POST['rtsb_vg'][ $variation_id ] ) ) {
159 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Verified by check_ajax_referer() above.
160 $rtsb_vg_ids = array_map( 'absint', wp_unslash( $_POST['rtsb_vg'][ $variation_id ] ) );
161 $rtsb_vg_ids = array_values( array_unique( array_filter( $rtsb_vg_ids ) ) );
162 }
163
164 if ( GalleryFns::use_native_gallery() ) {
165 $variation = wc_get_product( $variation_id );
166
167 if ( $variation && $variation->is_type( 'variation' ) ) {
168 $variation->set_gallery_image_ids( $rtsb_vg_ids );
169 $variation->save();
170
171 // Stamped only once the native gallery has actually been written: the
172 // sentinel permanently suppresses the legacy fallback, so setting it
173 // after a failed load would strand that variation's images unreachable.
174 GalleryFns::mark_native_gallery_owned( $variation_id );
175 }
176 } elseif ( $rtsb_vg_ids ) {
177 update_post_meta( $variation_id, GalleryFns::LEGACY_GALLERY_META_KEY, $rtsb_vg_ids );
178 } else {
179 delete_post_meta( $variation_id, GalleryFns::LEGACY_GALLERY_META_KEY );
180 }
181
182 GalleryFns::delete_transients( $variation_id, 'variation-images' );
183 }
184
185 /**
186 * Keep the stale legacy gallery meta out of CSV exports.
187 *
188 * Once a variation is native-owned the legacy meta is a snapshot, not the truth:
189 * the migration copies rather than moves it, and later saves only touch the native
190 * store. Exporting it would put outdated data in the CSV, and re-importing that
191 * file could write it back over a gallery the merchant has since changed.
192 *
193 * WooCommerce exports the native gallery itself through the standard `Images`
194 * column, so nothing is lost by omitting this key.
195 *
196 * @param mixed $meta_value Meta value being exported.
197 * @param object $meta Meta object.
198 * @param \WC_Product $product Product or variation being exported.
199 *
200 * @return mixed
201 */
202 public function product_export_meta_value( $meta_value, $meta, $product ) {
203 if ( ! GalleryFns::use_native_gallery() ) {
204 return $meta_value;
205 }
206
207 if ( ! isset( $meta->key ) || GalleryFns::LEGACY_GALLERY_META_KEY !== $meta->key ) {
208 return $meta_value;
209 }
210
211 if ( $product instanceof \WC_Product && GalleryFns::is_native_gallery_owned( $product->get_id() ) ) {
212 return '';
213 }
214
215 return $meta_value;
216 }
217
218 /**
219 * Hide WooCommerce's own variation gallery field in the product editor.
220 *
221 * This module keeps its existing metabox as the single authoring UI and writes
222 * straight into the native store, so rendering both fields would give merchants two
223 * controls for one value. WooCommerce's field markup is also what its admin CSS
224 * keys off (`:has(.wc-variation-gallery-field)`), so removing the render leaves the
225 * stock variation image slot untouched and the stylesheet inert.
226 *
227 * Nothing is suppressed when this module is inactive — that code path never runs,
228 * so WooCommerce keeps full ownership of its own UI.
229 *
230 * @return void
231 */
232 public function remove_native_variation_gallery_field() {
233 $class = 'Automattic\WooCommerce\Internal\VariationGallery\ClassicVariationGalleryAdmin';
234 $hook = 'woocommerce_variation_after_upload_image';
235
236 if ( ! GalleryFns::use_native_gallery() || ! class_exists( $class ) ) {
237 return;
238 }
239
240 if ( ! apply_filters( 'rtsb/vg/remove/native/variation/gallery/field', true ) ) {
241 return;
242 }
243
244 if ( empty( $GLOBALS['wp_filter'][ $hook ] ) ) {
245 return;
246 }
247
248 // Matched on the registered object rather than resolved from WooCommerce's
249 // container, so removal does not depend on the container handing back the very
250 // same instance it registered the callback with.
251 foreach ( $GLOBALS['wp_filter'][ $hook ]->callbacks as $priority => $callbacks ) {
252 foreach ( $callbacks as $callback ) {
253 if ( is_array( $callback['function'] ) && isset( $callback['function'][0] ) && $callback['function'][0] instanceof $class ) {
254 remove_action( $hook, $callback['function'], $priority );
255 }
256 }
257 }
258 }
259 /**
260 * @return void
261 */
262 public function admin_template_js() {
263 require_once RTSB_PATH . '/app/Modules/VariationGallery/view/template-admin-thumbnail.php';
264 }
265
266 /**
267 * Outputs the variation gallery image section in the product variation admin panel.
268 *
269 * Enqueues the necessary admin script and renders the gallery UI for a specific
270 * variation, allowing admin users to add, preview, and remove multiple images
271 * associated with a WooCommerce product variation.
272 *
273 * @param int $loop The index of the current variation loop.
274 * @param array $variation_data Array of variation data.
275 * @param WP_Post $variation The variation object (as a WP_Post).
276 *
277 * @return void
278 */
279 public function gallery_admin_html( $loop, $variation_data, $variation ) {
280 $variation_id = absint( $variation->ID );
281 $gallery_images = null;
282
283 // With the native gallery available it is authoritative, so the metabox shows it
284 // rather than the legacy meta it mirrors — and never reads that meta, which on a
285 // migrated store may no longer exist. The legacy value is only consulted while
286 // this variation is still waiting on the migration; once the sentinel is set, an
287 // empty native gallery is a deliberate choice and is shown as empty.
288 if ( GalleryFns::use_native_gallery() ) {
289 $variation_object = wc_get_product( $variation_id );
290
291 if ( $variation_object && $variation_object->is_type( 'variation' ) ) {
292 $native_images = GalleryFns::get_native_variation_gallery_ids( $variation_object );
293
294 if ( $native_images || GalleryFns::is_native_gallery_owned( $variation_id ) ) {
295 $gallery_images = $native_images;
296 }
297 }
298 }
299
300 if ( null === $gallery_images ) {
301 $gallery_images = GalleryFns::get_legacy_variation_gallery_ids( $variation_id );
302 }
303
304 /*
305 * WooCommerce renders the variation's main image at thumbnail size (150px) in
306 * `html-variation-admin.php`, which is soft in the wider slot this module uses.
307 * Its own variation gallery hero uses `woocommerce_single`, so the matching URL
308 * is handed to the admin script here — that markup belongs to core and exposes
309 * no filter to size it directly.
310 */
311 $variation_image_id = absint( get_post_thumbnail_id( $variation_id ) );
312 $hero_src = $variation_image_id ? wp_get_attachment_image_url( $variation_image_id, 'woocommerce_single' ) : '';
313 ?>
314 <div class="form-row form-row-full rtsb-vg-gallery-wrapper" data-hero-src="<?php echo esc_url( $hero_src ); ?>">
315 <h4><?php esc_html_e( 'Variation Image Gallery', 'shopbuilder' ); ?></h4>
316 <div class="rtsb-vg-image-container">
317 <ul class="rtsb-vg-images">
318 <?php
319 if ( is_array( $gallery_images ) && ! empty( $gallery_images ) ) {
320 $gallery_images = array_values( array_unique( $gallery_images ) );
321 foreach ( $gallery_images as $image_id ) :
322 $image = wp_get_attachment_image_src( $image_id );
323 /**
324 * Functions::gallery_has_video( $image_id );.
325 */
326 if ( empty( $image[0] ) ) {
327 continue;
328 }
329 $add_video_class = '';
330 if ( rtsb()->has_pro() ) {
331 $video = trim( get_post_meta( $image_id, 'rtsb_vg_video_link', true ) ?? '' );
332 $add_video_class = ! empty( $video ) ? ' video' : '';
333 }
334 ?>
335 <li class="image<?php echo esc_html( $add_video_class ); ?>">
336 <input type="hidden" name="rtsb_vg[<?php echo absint( $variation_id ); ?>][]" value="<?php echo absint( $image_id ); ?>">
337 <img src="<?php echo esc_url( $image[0] ); ?>">
338 <div class="rtsb-vg-action-button">
339 <span data-tip="<?php esc_attr_e( 'Add Video', 'shopbuilder' ); ?>" class="rtsb-vg-media-video-popup woocommerce-help-tip dashicons dashicons-video-alt3"></span>
340 <span data-tip="<?php esc_attr_e( 'Edit Image', 'shopbuilder' ); ?>" class="rtsb-vg-gallery-edit woocommerce-help-tip dashicons dashicons-edit"></span>
341 <a href="#" class="delete rtsb-vg-remove-image woocommerce-help-tip" data-tip="<?php esc_attr_e( 'Remove', 'shopbuilder' ); ?>"><span class="dashicons dashicons-no"></span></a>
342 </div>
343 </li>
344 <?php
345 endforeach;
346 }
347 ?>
348 </ul>
349 </div>
350 <p class="rtsb-vg-add-image-wrapper hide-if-no-js">
351 <a href="#" data-product_variation_loop="<?php echo absint( $loop ); ?>"
352 data-product_variation_id="<?php echo esc_attr( $variation_id ); ?>"
353 class="button rtsb-vg-add-image">
354 <?php
355 esc_html_e( 'Add Gallery Images', 'shopbuilder' );
356 ?>
357 </a>
358 </p>
359 </div>
360 <?php
361 }
362 }