Преглед на файлове

Add min/max image resolution validation — v1.2.0 -> v1.2.1

- Add Min Image Resolution and Max Image Resolution fields to Free
  Photo product tab (width x height in pixels)
- Width and height are commutable: a 3000x2000 image passes both
  3000x2000 and 2000x3000 limits (portrait/landscape auto-detected)
- Validated server-side after file assembly, before attachment creation
- RAW files where getimagesize() fails are skipped (no validation)
- Empty/zero values mean no limit
- Add Czech translations for all new strings
- Update documentation and product meta keys table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dalibor Votruba преди 3 месеца
родител
ревизия
5f4489da6c

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

@@ -2,7 +2,7 @@
 
 
 ## Project Overview
 ## Project Overview
 
 
-WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.2.0.
+WordPress/WooCommerce plugin called **studiou-wc-free-photo-product** (QDR - Studiou WC Free Photo Product) v1.2.1.
 Allows publishing special variable products where customers upload custom raw images to be printed for a price.
 Allows publishing special variable products where customers upload custom raw images to be printed for a price.
 
 
 ## Initial Requirements
 ## Initial Requirements

+ 45 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-product.php

@@ -32,6 +32,10 @@ class Studiou_WC_FPP_Product {
         $enabled = get_post_meta($product_id, '_studiou_fpp_enabled', true);
         $enabled = get_post_meta($product_id, '_studiou_fpp_enabled', true);
         $media_category = get_post_meta($product_id, '_studiou_fpp_media_category', true);
         $media_category = get_post_meta($product_id, '_studiou_fpp_media_category', true);
         $max_file_size = get_post_meta($product_id, '_studiou_fpp_max_file_size', true);
         $max_file_size = get_post_meta($product_id, '_studiou_fpp_max_file_size', true);
+        $min_width = get_post_meta($product_id, '_studiou_fpp_min_width', true);
+        $min_height = get_post_meta($product_id, '_studiou_fpp_min_height', true);
+        $max_width = get_post_meta($product_id, '_studiou_fpp_max_width', true);
+        $max_height = get_post_meta($product_id, '_studiou_fpp_max_height', true);
 
 
         if (empty($max_file_size)) {
         if (empty($max_file_size)) {
             $max_file_size = 50;
             $max_file_size = 50;
@@ -100,6 +104,27 @@ class Studiou_WC_FPP_Product {
                 ?>
                 ?>
             </div>
             </div>
 
 
+            <div class="options_group">
+                <p class="form-field">
+                    <label><?php esc_html_e('Min Image Resolution (px)', 'studiou-wc-free-photo-product'); ?></label>
+                    <span class="wrap">
+                        <input type="number" id="_studiou_fpp_min_width" name="_studiou_fpp_min_width" class="short" style="width:80px;" min="0" step="1" value="<?php echo esc_attr($min_width); ?>" placeholder="<?php esc_attr_e('Width', 'studiou-wc-free-photo-product'); ?>" />
+                        &times;
+                        <input type="number" id="_studiou_fpp_min_height" name="_studiou_fpp_min_height" class="short" style="width:80px;" min="0" step="1" value="<?php echo esc_attr($min_height); ?>" placeholder="<?php esc_attr_e('Height', 'studiou-wc-free-photo-product'); ?>" />
+                    </span>
+                    <?php echo wc_help_tip(__('Minimum image resolution in pixels. Leave empty to skip. Width and height are interchangeable (portrait/landscape auto-detected).', 'studiou-wc-free-photo-product')); ?>
+                </p>
+                <p class="form-field">
+                    <label><?php esc_html_e('Max Image Resolution (px)', 'studiou-wc-free-photo-product'); ?></label>
+                    <span class="wrap">
+                        <input type="number" id="_studiou_fpp_max_width" name="_studiou_fpp_max_width" class="short" style="width:80px;" min="0" step="1" value="<?php echo esc_attr($max_width); ?>" placeholder="<?php esc_attr_e('Width', 'studiou-wc-free-photo-product'); ?>" />
+                        &times;
+                        <input type="number" id="_studiou_fpp_max_height" name="_studiou_fpp_max_height" class="short" style="width:80px;" min="0" step="1" value="<?php echo esc_attr($max_height); ?>" placeholder="<?php esc_attr_e('Height', 'studiou-wc-free-photo-product'); ?>" />
+                    </span>
+                    <?php echo wc_help_tip(__('Maximum image resolution in pixels. Leave empty to skip. Width and height are interchangeable (portrait/landscape auto-detected).', 'studiou-wc-free-photo-product')); ?>
+                </p>
+            </div>
+
             <div class="options_group">
             <div class="options_group">
                 <p class="form-field">
                 <p class="form-field">
                     <label><?php esc_html_e('Shortcode', 'studiou-wc-free-photo-product'); ?></label>
                     <label><?php esc_html_e('Shortcode', 'studiou-wc-free-photo-product'); ?></label>
@@ -125,6 +150,13 @@ class Studiou_WC_FPP_Product {
             if ($max_size > 500) $max_size = 500;
             if ($max_size > 500) $max_size = 500;
             update_post_meta($product_id, '_studiou_fpp_max_file_size', $max_size);
             update_post_meta($product_id, '_studiou_fpp_max_file_size', $max_size);
         }
         }
+
+        // Resolution limits (0 or empty = no limit)
+        $res_fields = array('_studiou_fpp_min_width', '_studiou_fpp_min_height', '_studiou_fpp_max_width', '_studiou_fpp_max_height');
+        foreach ($res_fields as $field) {
+            $val = isset($_POST[$field]) ? absint($_POST[$field]) : 0;
+            update_post_meta($product_id, $field, $val ?: '');
+        }
     }
     }
 
 
     public function ajax_add_media_category() {
     public function ajax_add_media_category() {
@@ -165,4 +197,17 @@ class Studiou_WC_FPP_Product {
     public static function get_product_media_category($product_id) {
     public static function get_product_media_category($product_id) {
         return (int) get_post_meta($product_id, '_studiou_fpp_media_category', true);
         return (int) get_post_meta($product_id, '_studiou_fpp_media_category', true);
     }
     }
+
+    /**
+     * Get resolution limits for a product.
+     * Returns array with min_width, min_height, max_width, max_height (0 = no limit).
+     */
+    public static function get_product_resolution_limits($product_id) {
+        return array(
+            'min_width'  => (int) get_post_meta($product_id, '_studiou_fpp_min_width', true),
+            'min_height' => (int) get_post_meta($product_id, '_studiou_fpp_min_height', true),
+            'max_width'  => (int) get_post_meta($product_id, '_studiou_fpp_max_width', true),
+            'max_height' => (int) get_post_meta($product_id, '_studiou_fpp_max_height', true),
+        );
+    }
 }
 }

+ 68 - 0
studiou-wc-free-photo-product/includes/class-studiou-wc-fpp-upload.php

@@ -189,6 +189,13 @@ class Studiou_WC_FPP_Upload {
             return new WP_Error('invalid_type', __('File type not allowed.', 'studiou-wc-free-photo-product'));
             return new WP_Error('invalid_type', __('File type not allowed.', 'studiou-wc-free-photo-product'));
         }
         }
 
 
+        // Validate image resolution (if limits are set)
+        $res_error = $this->validate_image_resolution($assembled_path, $product_id);
+        if (is_wp_error($res_error)) {
+            unlink($assembled_path);
+            return $res_error;
+        }
+
         // Create WP attachment
         // Create WP attachment
         $relative_path = str_replace($upload_dir['basedir'] . '/', '', $assembled_path);
         $relative_path = str_replace($upload_dir['basedir'] . '/', '', $assembled_path);
         $filetype = wp_check_filetype($unique_name, $allowed);
         $filetype = wp_check_filetype($unique_name, $allowed);
@@ -278,6 +285,67 @@ class Studiou_WC_FPP_Upload {
         );
         );
     }
     }
 
 
+    /**
+     * Validate image resolution against product limits.
+     * Width/height are commutable — a 3000x2000 image matches both 3000x2000 and 2000x3000 limits.
+     */
+    private function validate_image_resolution($file_path, $product_id) {
+        $limits = Studiou_WC_FPP_Product::get_product_resolution_limits($product_id);
+
+        // Skip if no limits set
+        $has_min = ($limits['min_width'] > 0 || $limits['min_height'] > 0);
+        $has_max = ($limits['max_width'] > 0 || $limits['max_height'] > 0);
+        if (!$has_min && !$has_max) {
+            return true;
+        }
+
+        // Get image dimensions
+        $size = @getimagesize($file_path);
+        if (!$size || !isset($size[0], $size[1])) {
+            // Cannot determine dimensions (e.g. RAW files) — skip validation
+            return true;
+        }
+
+        $img_w = $size[0];
+        $img_h = $size[1];
+
+        // Normalize: always compare min(w,h) vs min(limit_w,limit_h) and max(w,h) vs max(limit_w,limit_h)
+        // This makes portrait/landscape interchangeable
+        $img_short = min($img_w, $img_h);
+        $img_long = max($img_w, $img_h);
+
+        // Minimum resolution check
+        if ($has_min) {
+            $min_short = min($limits['min_width'] ?: 0, $limits['min_height'] ?: 0);
+            $min_long = max($limits['min_width'] ?: 0, $limits['min_height'] ?: 0);
+            // If only one dimension is set, use it for both
+            if ($min_short === 0) $min_short = $min_long;
+
+            if ($img_short < $min_short || $img_long < $min_long) {
+                return new WP_Error('resolution_too_small', sprintf(
+                    __('Image resolution %1$dx%2$d px is too small. Minimum required: %3$dx%4$d px.', 'studiou-wc-free-photo-product'),
+                    $img_w, $img_h, $limits['min_width'] ?: $limits['min_height'], $limits['min_height'] ?: $limits['min_width']
+                ));
+            }
+        }
+
+        // Maximum resolution check
+        if ($has_max) {
+            $max_short = min($limits['max_width'] ?: PHP_INT_MAX, $limits['max_height'] ?: PHP_INT_MAX);
+            $max_long = max($limits['max_width'] ?: PHP_INT_MAX, $limits['max_height'] ?: PHP_INT_MAX);
+            if ($max_short === PHP_INT_MAX) $max_short = $max_long;
+
+            if ($img_short > $max_short || $img_long > $max_long) {
+                return new WP_Error('resolution_too_large', sprintf(
+                    __('Image resolution %1$dx%2$d px is too large. Maximum allowed: %3$dx%4$d px.', 'studiou-wc-free-photo-product'),
+                    $img_w, $img_h, $limits['max_width'] ?: $limits['max_height'], $limits['max_height'] ?: $limits['max_width']
+                ));
+            }
+        }
+
+        return true;
+    }
+
     private function cleanup_chunks($upload_id, $total_chunks) {
     private function cleanup_chunks($upload_id, $total_chunks) {
         $chunks_dir = $this->get_chunks_dir();
         $chunks_dir = $this->get_chunks_dir();
         for ($i = 0; $i < $total_chunks; $i++) {
         for ($i = 0; $i < $total_chunks; $i++) {

+ 25 - 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.
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgid ""
 msgstr ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.2.0\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.2.1\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: 2026-04-02 00:00+0000\n"
@@ -148,6 +148,30 @@ msgstr "Shortcode"
 msgid "Use this shortcode to display this product with photo upload on any page."
 msgid "Use this shortcode to display this product with photo upload on any page."
 msgstr "Použijte tento shortcode pro zobrazení tohoto produktu s nahráváním fotografií na jakékoli stránce."
 msgstr "Použijte tento shortcode pro zobrazení tohoto produktu s nahráváním fotografií na jakékoli stránce."
 
 
+msgid "Min Image Resolution (px)"
+msgstr "Min. rozlišení obrázku (px)"
+
+msgid "Width"
+msgstr "Šířka"
+
+msgid "Height"
+msgstr "Výška"
+
+msgid "Minimum image resolution in pixels. Leave empty to skip. Width and height are interchangeable (portrait/landscape auto-detected)."
+msgstr "Minimální rozlišení obrázku v pixelech. Ponechte prázdné pro přeskočení. Šířka a výška jsou zaměnitelné (automatická detekce orientace)."
+
+msgid "Max Image Resolution (px)"
+msgstr "Max. rozlišení obrázku (px)"
+
+msgid "Maximum image resolution in pixels. Leave empty to skip. Width and height are interchangeable (portrait/landscape auto-detected)."
+msgstr "Maximální rozlišení obrázku v pixelech. Ponechte prázdné pro přeskočení. Šířka a výška jsou zaměnitelné (automatická detekce orientace)."
+
+msgid "Image resolution %1$dx%2$d px is too small. Minimum required: %3$dx%4$d px."
+msgstr "Rozlišení obrázku %1$dx%2$d px je příliš malé. Minimální požadované: %3$dx%4$d px."
+
+msgid "Image resolution %1$dx%2$d px is too large. Maximum allowed: %3$dx%4$d px."
+msgstr "Rozlišení obrázku %1$dx%2$d px je příliš velké. Maximální povolené: %3$dx%4$d px."
+
 msgid "Category name is required."
 msgid "Category name is required."
 msgstr "Název kategorie je povinný."
 msgstr "Název kategorie je povinný."
 
 

+ 35 - 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.
 # This file is distributed under the GPL v2 or later.
 msgid ""
 msgid ""
 msgstr ""
 msgstr ""
-"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.2.0\n"
+"Project-Id-Version: QDR - Studiou WC Free Photo Product 1.2.1\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "Report-Msgid-Bugs-To: https://www.quadarax.com\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "POT-Creation-Date: 2026-04-02 00:00+0000\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
@@ -190,6 +190,40 @@ msgstr ""
 msgid "Use this shortcode to display this product with photo upload on any page."
 msgid "Use this shortcode to display this product with photo upload on any page."
 msgstr ""
 msgstr ""
 
 
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Min Image Resolution (px)"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Width"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Height"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Minimum image resolution in pixels. Leave empty to skip. Width and height are interchangeable (portrait/landscape auto-detected)."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Max Image Resolution (px)"
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-product.php
+msgid "Maximum image resolution in pixels. Leave empty to skip. Width and height are interchangeable (portrait/landscape auto-detected)."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+#, php-format
+msgid "Image resolution %1$dx%2$d px is too small. Minimum required: %3$dx%4$d px."
+msgstr ""
+
+#: includes/class-studiou-wc-fpp-upload.php
+#, php-format
+msgid "Image resolution %1$dx%2$d px is too large. Maximum allowed: %3$dx%4$d px."
+msgstr ""
+
 #: includes/class-studiou-wc-fpp-product.php
 #: includes/class-studiou-wc-fpp-product.php
 msgid "Category name is required."
 msgid "Category name is required."
 msgstr ""
 msgstr ""

+ 6 - 1
studiou-wc-free-photo-product/readme.md

@@ -25,7 +25,8 @@ WordPress (6.9.4+) / WooCommerce (10.6.2+) plugin that allows publishing special
 4. Check **Enable Free Photo**
 4. Check **Enable Free Photo**
 5. Select or create a **Media Category** - uploaded files will be assigned to this category
 5. Select or create a **Media Category** - uploaded files will be assigned to this category
 6. Set **Max File Size (MB)** - maximum allowed upload size per file (1-500 MB, default 50)
 6. Set **Max File Size (MB)** - maximum allowed upload size per file (1-500 MB, default 50)
-7. Copy the displayed **Shortcode** for use on custom pages
+7. Optionally set **Min Image Resolution (px)** and **Max Image Resolution (px)** - width and height are interchangeable (portrait/landscape auto-detected). Leave empty to skip validation. RAW files that cannot be measured are skipped.
+8. Copy the displayed **Shortcode** for use on custom pages
 
 
 ### Product Detail Page
 ### Product Detail Page
 
 
@@ -231,6 +232,10 @@ studiou-wc-free-photo-product/
 | `_studiou_fpp_enabled` | "yes" / "no" |
 | `_studiou_fpp_enabled` | "yes" / "no" |
 | `_studiou_fpp_media_category` | Term ID of media category |
 | `_studiou_fpp_media_category` | Term ID of media category |
 | `_studiou_fpp_max_file_size` | Max upload size in MB |
 | `_studiou_fpp_max_file_size` | Max upload size in MB |
+| `_studiou_fpp_min_width` | Min image width in px (0 = no limit) |
+| `_studiou_fpp_min_height` | Min image height in px (0 = no limit) |
+| `_studiou_fpp_max_width` | Max image width in px (0 = no limit) |
+| `_studiou_fpp_max_height` | Max image height in px (0 = no limit) |
 
 
 ### Order Item Meta Keys
 ### Order Item Meta Keys
 
 

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