Selaa lähdekoodia

Server-side add-to-cart blocking, remove client-side button disabling — v1.0.7 -> v1.1.1

- Add woocommerce_add_to_cart_validation (priority 1) with
  resolve_fpp_product_id() checking product/parent/variation
- Add woocommerce_add_to_cart safety net that removes cart items
  added without photo data
- Remove all client-side button disabling (CSS dimming, name attr
  removal, MutationObserver, capture-phase listeners)
- Buttons look and behave normally — blocking is entirely server-side
- JS now only handles upload, preview, gallery replacement, session
- Update documentation and translations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dalibor Votruba 3 kuukautta sitten
vanhempi
commit
a9c4e2aca1

+ 9 - 9
studiou-wc-free-photo-product/CLAUDE.md

@@ -2,7 +2,7 @@
 
 ## Project Overview
 
-WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.0.7.
+WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.1.1.
 Allows publishing special variable products where customers upload custom raw images to be printed for a price.
 
 ## Initial Requirements
@@ -36,7 +36,7 @@ Allows publishing special variable products where customers upload custom raw im
 - `Studiou_WC_FPP_DB` - Custom table CRUD, media category taxonomy
 - `Studiou_WC_FPP_Product` - WC product data tab, product meta
 - `Studiou_WC_FPP_Upload` - Chunked upload AJAX, file assembly, attachment creation
-- `Studiou_WC_FPP_Single_Product` - Upload area on product page (`woocommerce_single_product_summary` priority 35), WC session storage for file data, validates upload on add-to-cart
+- `Studiou_WC_FPP_Single_Product` - Upload area on product page (`woocommerce_single_product_summary` priority 35), WC session storage, server-side validation (`woocommerce_add_to_cart_validation` priority 1) + safety net (`woocommerce_add_to_cart` removes items without photo)
 - `Studiou_WC_FPP_Shortcode` - `[studiou_free_photo_product id="X"]` renders product with WC native variation form
 - `Studiou_WC_FPP_Cart` - Reads file data from WC session via `woocommerce_add_cart_item_data`, replaces cart/order item thumbnails with uploaded photo, order item meta, order linking
 - `Studiou_WC_FPP_Admin` - Summary page, status management, ZIP download
@@ -44,13 +44,13 @@ Allows publishing special variable products where customers upload custom raw im
 ## Frontend Flow
 
 1. Upload area rendered below variation table via `woocommerce_single_product_summary` (priority 35)
-2. All add-to-cart buttons disabled on page load (CSS + disabled + pointer-events + capture-phase listener + MutationObserver)
-3. On page load, existing session state restored (preview, gallery image, buttons enabled)
-4. Customer uploads photo via chunked AJAX
-5. On upload complete, JS stores file ref in WC session, enables add-to-cart buttons
-6. Product gallery image replaced with uploaded photo preview; restored on remove
-7. Customer selects variant and clicks any Add to Cart button (works with any theme/layout)
-8. `woocommerce_add_to_cart_validation` checks WC session for uploaded file (server-side fallback)
+2. On page load, existing session state restored (preview, gallery image)
+3. Customer uploads photo via chunked AJAX
+4. On upload complete, JS stores file ref in WC session
+5. Product gallery image replaced with uploaded photo preview; restored on remove
+6. Customer selects variant and clicks any Add to Cart button (buttons look normal — no client-side disabling)
+7. `woocommerce_add_to_cart_validation` (priority 1) checks WC session — blocks with error notice if no photo
+8. `woocommerce_add_to_cart` safety net — removes item if it was added without photo data
 9. `woocommerce_add_cart_item_data` reads file data from WC session (session NOT cleared — same photo reusable for multiple variants)
 10. Cart/order item thumbnails replaced with uploaded photo (`woocommerce_cart_item_thumbnail`, `woocommerce_admin_order_item_thumbnail`)
 11. On checkout, file record is linked to order via `woocommerce_checkout_order_processed`

+ 0 - 7
studiou-wc-free-photo-product/assets/css/frontend.css

@@ -236,13 +236,6 @@
     cursor: not-allowed;
 }
 
-/* Disabled add-to-cart buttons */
-.studiou-fpp-disabled {
-    opacity: 0.4 !important;
-    cursor: not-allowed !important;
-    pointer-events: none !important;
-}
-
 /* Messages */
 .studiou-fpp-msg {
     padding: 10px 15px;

+ 6 - 94
studiou-wc-free-photo-product/assets/js/frontend.js

@@ -16,19 +16,6 @@
         var isUploading = false;
         var originalGalleryImages = {};
 
-        // Broad selectors for add-to-cart elements
-        var addToCartSelectors = [
-            '.single_add_to_cart_button',
-            '.add_to_cart_button',
-            'button[name="add-to-cart"]',
-            'input[name="add-to-cart"]',
-            'a[data-product_id]',
-            'button[data-product_id]',
-            'a[href*="add-to-cart"]',
-            '.product form.cart button[type="submit"]',
-            '.product form.cart input[type="submit"]'
-        ].join(', ');
-
         // Upload area elements
         var $dropzone = $uploadWrap.find('#studiou-fpp-dropzone');
         var $fileInput = $uploadWrap.find('#studiou-fpp-file-input');
@@ -41,66 +28,6 @@
         var $removeBtn = $uploadWrap.find('.studiou-fpp-remove-file');
         var $messages = $uploadWrap.find('.studiou-fpp-messages');
 
-        // ==========================================
-        // Disable/enable add-to-cart buttons
-        // ==========================================
-        function disableAddToCart() {
-            $(addToCartSelectors).each(function () {
-                var $el = $(this);
-                $el.addClass('studiou-fpp-disabled');
-                if ($el.is('a')) {
-                    if (!$el.data('studiou-fpp-href')) {
-                        $el.data('studiou-fpp-href', $el.attr('href'));
-                    }
-                    $el.attr('href', '#');
-                } else {
-                    $el.prop('disabled', true);
-                }
-            });
-        }
-
-        function enableAddToCart() {
-            $(addToCartSelectors).each(function () {
-                var $el = $(this);
-                $el.removeClass('studiou-fpp-disabled');
-                if ($el.is('a')) {
-                    var origHref = $el.data('studiou-fpp-href');
-                    if (origHref) {
-                        $el.attr('href', origHref);
-                    }
-                } else {
-                    $el.prop('disabled', false);
-                }
-            });
-        }
-
-        // Fallback: intercept clicks on disabled elements (capture phase)
-        document.addEventListener('click', function (e) {
-            if (!uploadedAttachmentId) {
-                var el = e.target.closest(addToCartSelectors.replace(/,\s*/g, ', '));
-                if (el) {
-                    e.preventDefault();
-                    e.stopImmediatePropagation();
-                    showMessage(studiouWcfppFront.i18n.uploadFile, 'error');
-                    $('html, body').animate({ scrollTop: $uploadWrap.offset().top - 100 }, 400);
-                    return false;
-                }
-            }
-        }, true); // capture phase - fires before bubble
-
-        // Disable buttons on page load
-        disableAddToCart();
-
-        // Also watch for dynamically added buttons (e.g. AJAX-loaded variations)
-        var observer = new MutationObserver(function () {
-            if (!uploadedAttachmentId) {
-                disableAddToCart();
-            }
-        });
-        observer.observe(document.querySelector('.product') || document.body, {
-            childList: true, subtree: true
-        });
-
         // ==========================================
         // Restore session state on page load
         // ==========================================
@@ -109,7 +36,6 @@
             uploadedFileRecordId = studiouWcfppSession.file_record_id;
             showPreview(studiouWcfppSession.thumbnail_url, studiouWcfppSession.file_name);
             updateProductGallery(studiouWcfppSession.preview_url || studiouWcfppSession.thumbnail_url);
-            enableAddToCart();
         }
 
         // ==========================================
@@ -226,7 +152,6 @@
 
                                 showPreview(response.data.thumbnail_url, response.data.file_name);
                                 updateProductGallery(response.data.preview_url || response.data.thumbnail_url);
-                                enableAddToCart();
                                 $progress.hide();
                                 isUploading = false;
                             } else {
@@ -293,27 +218,19 @@
                 $img = $(selectors[i]).first();
                 if ($img.length) break;
             }
-
             if (!$img || !$img.length) return;
 
             if (!originalGalleryImages.src) {
                 originalGalleryImages.src = $img.attr('src');
                 originalGalleryImages.srcset = $img.attr('srcset') || '';
                 originalGalleryImages.sizes = $img.attr('sizes') || '';
-                var $parentLink = $img.closest('a');
-                if ($parentLink.length) {
-                    originalGalleryImages.href = $parentLink.attr('href');
-                }
+                var $link = $img.closest('a');
+                if ($link.length) originalGalleryImages.href = $link.attr('href');
             }
 
-            $img.attr('src', imageUrl);
-            $img.removeAttr('srcset');
-            $img.removeAttr('sizes');
-
-            var $parentLink = $img.closest('a');
-            if ($parentLink.length) {
-                $parentLink.attr('href', imageUrl);
-            }
+            $img.attr('src', imageUrl).removeAttr('srcset').removeAttr('sizes');
+            var $link = $img.closest('a');
+            if ($link.length) $link.attr('href', imageUrl);
         }
 
         function restoreProductGallery() {
@@ -333,16 +250,12 @@
                 $img = $(selectors[i]).first();
                 if ($img.length) break;
             }
-
             if (!$img || !$img.length) return;
 
             $img.attr('src', originalGalleryImages.src);
             if (originalGalleryImages.srcset) $img.attr('srcset', originalGalleryImages.srcset);
             if (originalGalleryImages.sizes) $img.attr('sizes', originalGalleryImages.sizes);
-
-            if (originalGalleryImages.href) {
-                $img.closest('a').attr('href', originalGalleryImages.href);
-            }
+            if (originalGalleryImages.href) $img.closest('a').attr('href', originalGalleryImages.href);
 
             originalGalleryImages = {};
         }
@@ -391,7 +304,6 @@
             uploadedFileRecordId = null;
             clearSession();
             restoreProductGallery();
-            disableAddToCart();
             $preview.hide();
             $previewImg.attr('src', '');
             $previewName.text('');

+ 57 - 14
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-single-product.php

@@ -8,8 +8,13 @@ class Studiou_WC_FPP_Single_Product {
     public function __construct() {
         // Render upload area below the variation table on single product page
         add_action('woocommerce_single_product_summary', array($this, 'render_upload_area'), 35);
-        // Validation before add to cart
-        add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 10, 3);
+
+        // Server-side validation: block add-to-cart without photo (priority 1 = very early)
+        add_filter('woocommerce_add_to_cart_validation', array($this, 'validate_add_to_cart'), 1, 6);
+
+        // Safety net: if item was added without photo, remove it immediately
+        add_action('woocommerce_add_to_cart', array($this, 'check_after_add_to_cart'), 10, 6);
+
         // AJAX: store uploaded file reference in WC session
         add_action('wp_ajax_studiou_wcfpp_set_upload_session', array($this, 'ajax_set_upload_session'));
         add_action('wp_ajax_nopriv_studiou_wcfpp_set_upload_session', array($this, 'ajax_set_upload_session'));
@@ -51,21 +56,42 @@ class Studiou_WC_FPP_Single_Product {
         include STUDIOU_WCFPP_PLUGIN_DIR . 'views/single-product-upload.php';
     }
 
-    public function validate_add_to_cart($passed, $product_id, $quantity) {
-        if (!Studiou_WC_FPP_Product::is_fpp_product($product_id)) {
-            // Also check parent product for variations
-            $product = wc_get_product($product_id);
-            if ($product && $product->get_parent_id()) {
-                $parent_id = $product->get_parent_id();
-                if (!Studiou_WC_FPP_Product::is_fpp_product($parent_id)) {
-                    return $passed;
-                }
-            } else {
-                return $passed;
+    /**
+     * Resolve whether a product_id or its parent is FPP-enabled.
+     */
+    private function resolve_fpp_product_id($product_id, $variation_id = 0) {
+        // Check direct product
+        if (Studiou_WC_FPP_Product::is_fpp_product($product_id)) {
+            return $product_id;
+        }
+
+        // Check parent of the product (in case product_id is variation)
+        $product = wc_get_product($product_id);
+        if ($product && $product->get_parent_id() && Studiou_WC_FPP_Product::is_fpp_product($product->get_parent_id())) {
+            return $product->get_parent_id();
+        }
+
+        // Check variation_id's parent
+        if ($variation_id && $variation_id !== $product_id) {
+            $variation = wc_get_product($variation_id);
+            if ($variation && $variation->get_parent_id() && Studiou_WC_FPP_Product::is_fpp_product($variation->get_parent_id())) {
+                return $variation->get_parent_id();
             }
         }
 
-        // Check WC session for uploaded file data
+        return false;
+    }
+
+    /**
+     * Validate before add-to-cart: block if FPP product and no photo in session.
+     */
+    public function validate_add_to_cart($passed, $product_id, $quantity, $variation_id = 0, $variations = array(), $cart_item_data = array()) {
+        $fpp_id = $this->resolve_fpp_product_id($product_id, $variation_id);
+        if (!$fpp_id) {
+            return $passed;
+        }
+
+        // Check WC session for uploaded file
         if (WC()->session) {
             $attachment_id = WC()->session->get('studiou_fpp_attachment_id');
             if ($attachment_id) {
@@ -77,6 +103,23 @@ class Studiou_WC_FPP_Single_Product {
         return false;
     }
 
+    /**
+     * Safety net: if an FPP item was added without photo data, remove it.
+     * This catches cases where validation was somehow bypassed.
+     */
+    public function check_after_add_to_cart($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
+        $fpp_id = $this->resolve_fpp_product_id($product_id, $variation_id);
+        if (!$fpp_id) {
+            return;
+        }
+
+        // If the cart item has no FPP attachment, remove it
+        if (empty($cart_item_data['studiou_fpp_attachment_id'])) {
+            WC()->cart->remove_cart_item($cart_item_key);
+            wc_add_notice(__('Please upload a photo before adding to cart.', 'studiou-wc-free-photo-product'), 'error');
+        }
+    }
+
     public function ajax_set_upload_session() {
         check_ajax_referer('studiou-wcfpp-front-nonce', 'nonce');
 

+ 1 - 1
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product-cs_CZ.po

@@ -3,7 +3,7 @@
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.7\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.1.1\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: 2026-04-02 00:00+0000\n"

+ 1 - 1
studiou-wc-free-photo-product/languages/studiou-wc-free-photo-product.pot

@@ -2,7 +2,7 @@
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.0.7\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.1.1\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"

+ 10 - 10
studiou-wc-free-photo-product/readme.md

@@ -31,20 +31,20 @@ WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special
 
 When a variable product has Free Photo enabled, the upload area automatically appears on the WooCommerce single product page **below** the product summary (below the variation table and Add to Cart buttons). The customer workflow is:
 
-1. All Add to Cart buttons are disabled until a photo is uploaded
-2. Upload a photo file via drag-and-drop or file browser (below the variation table)
-3. The product gallery image updates to show the uploaded photo preview
-4. Add to Cart buttons become enabled
-5. Select a variant from the product variation table and click its **Add to Cart** button
-6. The uploaded photo stays — the same photo can be used for multiple variants without re-uploading
+1. Upload a photo file via drag-and-drop or file browser (below the variation table)
+2. The product gallery image updates to show the uploaded photo preview
+3. Select a variant from the product variation table and click its **Add to Cart** button
+4. If no photo is uploaded, the server blocks the add-to-cart and shows an error notice
+5. The uploaded photo stays — the same photo can be used for multiple variants without re-uploading
 
 The uploaded file reference is stored in the WooCommerce session, so it works with any add-to-cart method (form POST, AJAX buttons, individual variation buttons). The session persists across add-to-cart actions, allowing the same photo to be used for multiple variants. The session is only cleared when the user explicitly removes the uploaded photo.
 
-Add-to-cart is blocked until a photo is uploaded:
-- **Client-side**: all add-to-cart buttons/links are disabled on page load (CSS + disabled attribute + pointer-events). A capture-phase event listener provides a fallback intercept. A MutationObserver re-disables dynamically added buttons.
-- **Server-side**: `woocommerce_add_to_cart_validation` checks the WC session for uploaded file data.
+Add-to-cart is blocked server-side until a photo is uploaded:
+- **Validation**: `woocommerce_add_to_cart_validation` (priority 1) checks the WC session for uploaded file data and blocks with an error notice if missing.
+- **Safety net**: `woocommerce_add_to_cart` action removes any FPP cart item that was added without a photo (catches edge cases where validation was bypassed).
+- Product detection uses `resolve_fpp_product_id()` which checks the product itself, its parent, and the variation's parent to handle all add-to-cart methods.
 
-On page reload, existing session state is restored — the upload preview, product gallery image, and enabled buttons are all reconstructed from the server-side session data. When the photo is removed, the original product image is restored and buttons are disabled again.
+On page reload, existing session state is restored — the upload preview and product gallery image are reconstructed from the server-side session data. When the photo is removed, the original product image is restored. Add-to-cart buttons always look and behave normally — blocking is entirely server-side.
 
 ### Shortcode
 

+ 2 - 2
studiou-wc-free-photo-product/studiou-wc-free-photo-product.php

@@ -3,7 +3,7 @@
  * Plugin Name: QDR - Studiou WC Free Photo Product
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-free-photo-product
  * Description: Allows publishing special WooCommerce products with variants and custom raw image upload for photo printing
- * Version: 1.0.7
+ * Version: 1.1.1
  * Requires at least: 6.9.4
  * Requires PHP: 8.2
  * Author: Dalibor Votruba
@@ -21,7 +21,7 @@ if (!defined('WPINC')) {
     die;
 }
 
-if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.0.7');
+if (!defined('STUDIOU_WCFPP_VERSION')) define('STUDIOU_WCFPP_VERSION', '1.1.1');
 if (!defined('STUDIOU_WCFPP_PLUGIN_DIR')) define('STUDIOU_WCFPP_PLUGIN_DIR', plugin_dir_path(__FILE__));
 if (!defined('STUDIOU_WCFPP_PLUGIN_URL')) define('STUDIOU_WCFPP_PLUGIN_URL', plugin_dir_url(__FILE__));
 if (!defined('STUDIOU_WCFPP_PLUGIN_BASENAME')) define('STUDIOU_WCFPP_PLUGIN_BASENAME', plugin_basename(__FILE__));