# QDR - Studiou WC Export/Import Product Categories **Version: 1.7.2** A WordPress plugin that extends WooCommerce to provide batch export and import functionality for both product categories and products, along with category manipulation tools. ## Description QDR - Studiou WC Export/Import Product Categories is a powerful WordPress plugin for WooCommerce store administrators who need to manage product categories and products in bulk. This plugin allows you to: - Export and import product categories with all their settings - Export and import variable products and their variations - Manipulate categories (move products, batch update descriptions, delete products) - Support for WooCommerce Protected Categories extension The plugin provides a complete solution for bulk product and category management with comprehensive validation, error reporting, and progress tracking. ## Features ### Category Management - Export all product categories to a CSV file - Import product categories from a CSV file - Add, update, or delete product categories in batch - Support for WooCommerce Protected Categories visibility settings - **Move products between categories** in bulk - **Batch description update** for multiple categories - **Delete products in category** - permanently delete all products and variants from selected categories - **Clear Media Categories** - permanently delete all media files from selected media categories (NEW in v1.5) - **Remove Not Assigned Media** - permanently delete all unused uncategorized media files (NEW in v1.5.2) - **Update product variant prices** - bulk-update regular price of variations by category + attribute filter (NEW in v1.5.3) - **Tabbed Category Manipulations page** - each operation is a tab whose content loads on demand, preventing page-load timeouts on large stores (NEW in v1.6.0) ### Product Management (NEW in v1.4) - **Export products to CSV** - Export variable products and variations by category selection - **Import products from CSV** - Batch create/update variable products and variations - Full support for product attributes and variations - Image URL handling with validation - Min/Max quantity support - Comprehensive import rules with validation - Failed products download for retry ### General Features - User-friendly interface integrated with WooCommerce admin - Real-time progress tracking for bulk operations - Comprehensive validation and error reporting - Full translation support (English/Czech) ## Requirements - WordPress 6.8.1 or higher - PHP 8.2 or higher - WooCommerce 9.8 or higher - WooCommerce Protected Categories 2.7 or higher ## Installation 1. Download the plugin ZIP file 2. Go to WordPress Dashboard > Plugins > Add New 3. Click "Upload Plugin" and select the ZIP file 4. Click "Install Now" 5. Activate the plugin ## CSV Formats ### Product Category CSV Format The plugin uses CSV files with the following columns for categories: | Column | Description | |--------|-------------| | name | Product Category Name | | url-name | Product Category URL slug | | parent-name | Parent Product Category Name (empty if no parent) | | description | Product Category description | | display-type | Product Category display type (values: 'default', 'products', 'subcategories', 'both') | | visibility | Product Category visibility (values: 'public', 'protected') | | protected-passwords | Pipe (|) separated passwords if visibility is 'protected' | | operation | Record operation: 'A' - add, 'U' - update, 'D' - delete | ### Product CSV Format (NEW in v1.4) The plugin uses CSV files with the following columns for products: | Column | Description | |--------|-------------| | ID | Product ID (0 for new products) | | Typ | Product type (only "variable" or "variation" allowed) | | Katalogové číslo | SKU (must be unique) | | Jméno | Product name | | Nadřazené | Parent product SKU (for variations only) | | Krátký popis | Short description | | Popis | Description | | Název 1 vlastnosti | Attribute 1 name | | Hodnota(y) 1 vlastnosti | Attribute 1 value(s) (pipe-separated for variable products) | | Kategorie | Categories (pipe-separated) | | Obrázky | Image URL (must be valid and accessible) | | Běžná cena | Regular price (required for variations) | | Vlastnost 1 viditelnost | Attribute 1 visibility (1 or 0) | | Vlastnost 1 globální | Attribute 1 global (1 or 0) | | Vlastnost 1 varianta | Attribute 1 used for variations (1 or 0) | | Povolit zákaznické recenze? | Enable reviews (1 or 0) | | Minimum Quantity | Minimum order quantity | | Maximum Quantity | Maximum order quantity | ## Usage ### Exporting Product Categories 1. Go to Products > Export Product Categories 2. Click "Export Product Categories" 3. Download the generated CSV file ### Importing Product Categories 1. Go to Products > Import Product Categories 2. Select your CSV file 3. Click "Import Product Categories" 4. Review the import results ### Exporting Products (NEW in v1.4) 1. Go to Products > Product Export 2. Select categories using checkboxes - Use "Select All" to select all categories - Use "Deselect All" to deselect all categories 3. Click "Export Selected Products" 4. Download the generated CSV file ### Importing Products (NEW in v1.4) 1. Go to Products > Product Import 2. Select your CSV file (UTF-8 encoding) 3. Click "Process Import File" 4. Watch the progress bar as products are being imported in batches - Products are processed 5 at a time to avoid timeout issues - Progress shows "Processing: X / Y products" 5. Review the import results showing: - Successful imports - Failed imports - Skipped items - Detailed messages for skipped/failed items only 6. If there are failed items, click "Save Failed Products" to download a CSV file with failed items for correction and re-import **Product Import Features:** - **Batch Processing**: Processes 5 products per batch to avoid PHP timeouts - **Auto-Create Categories**: Missing categories are automatically created during import - **Real-Time Progress**: Shows exact progress (e.g., "Processing: 15 / 50 products") - **Smart Category Matching**: Tries exact name, slug, and case-insensitive matching - **Image Optimization**: 30-second timeout per image, intermediate sizes disabled for speed - **Detailed Logging**: All operations logged for debugging (prefix: "STUDIOU WC IMPORT:") **Product Import Rules:** - Rule 1: All operations performed via WooCommerce API (not direct SQL) - Rule 2: If ID = 0, item will be created; otherwise updated - Rule 3: Only "variable" or "variation" types are allowed - Rule 4: SKU (Katalogové číslo) must be unique in WooCommerce database - Rule 5: Image URL must be valid format (accessibility checked during download) - Rule 6: Parent reference (Nadřazené) must point to an existing variable product SKU - Rule 7: Variations must have a regular price (Běžná cena) **Important Notes:** - Categories have unique names throughout the system (no hierarchy path needed) - Category names can contain dashes, dots, and special characters - Failed image downloads don't stop the import, they're just logged - Import can handle large files thanks to batch processing ### Category Manipulations #### Moving Products Between Categories 1. Go to Products > Category Manipulations 2. Select the source category (from which products will be moved) 3. Select the target category (to which products will be moved) 4. Click "Do" to execute the move operation 5. Review the results showing how many products were moved **Important Notes about Moving Products:** - Products can be assigned to multiple categories. This operation will remove the product from the source category and add it to the target category. - If a product is only assigned to the source category, it will be moved entirely to the target category. - If a product is assigned to multiple categories including the source category, it will remain in the other categories and be moved to the target category. - The operation processes all products regardless of their status (published, draft, private). #### Batch Description Between Categories 1. Go to Products > Category Manipulations 2. Scroll to the "Batch description between categories" section 3. Select one or more categories using the checkboxes - Use "Check All" to select all categories - Use "Uncheck All" to deselect all categories 4. Enter the description text in the textarea 5. Click "Do" to apply the description to all selected categories 6. Review the results showing how many categories were updated **Important Notes about Batch Description:** - The operation will overwrite the existing descriptions for all selected categories - Categories can be selected individually or in bulk using the check/uncheck all buttons - The description field supports multi-line text - All selected categories will receive exactly the same description #### Delete Products in Category 1. Go to Products > Category Manipulations 2. Scroll to the "Delete products in category" section 3. Select one or more categories using the checkboxes - Use "Check All" to select all categories - Use "Uncheck All" to deselect all categories 4. Click "Delete" button (you will be asked to confirm) 5. Watch the progress bar as products are being deleted in batches 6. Review the deletion statistics showing: - Total products deleted - Failed deletions (if any) - Failed product IDs (if any) **Important Notes about Delete Products:** - **THIS OPERATION PERMANENTLY DELETES ALL PRODUCTS AND THEIR VARIANTS** from the selected categories - **THIS ACTION CANNOT BE UNDONE!** - Products are deleted using WooCommerce operations to ensure proper cleanup - Variable products will have all their variations deleted automatically - Products are processed in batches of 10 to prevent timeouts - A real-time progress bar shows the deletion progress - If a product belongs to multiple categories, it will still be deleted entirely from the system - The page will automatically reload after successful deletion to update category counts #### Clear Media Categories (NEW in v1.5) 1. Go to Products > Category Manipulations 2. Scroll to the "Clear Media Categories" section 3. Select one or more media categories using the checkboxes - Use "Check All" to select all media categories - Use "Uncheck All" to deselect all media categories 4. Click "Delete" button (you will be asked to confirm) 5. Watch the progress bar as media files are being deleted in batches 6. Review the deletion statistics showing: - Total media files deleted - Failed deletions (if any) - Failed media IDs (if any) **Important Notes about Clear Media Categories:** - **THIS OPERATION PERMANENTLY DELETES ALL MEDIA FILES** from the selected media categories - **The files will be removed from the server. THIS ACTION CANNOT BE UNDONE!** - Requires a media category plugin (e.g., Media Library Categories) to be installed - Media files are deleted using `wp_delete_attachment()` for proper cleanup - Media files are processed in batches of 10 to prevent timeouts - A real-time progress bar shows the deletion progress - The page will automatically reload after successful deletion to update category counts #### Remove Not Assigned Media (NEW in v1.5.2) 1. Go to Products > Category Manipulations 2. Scroll to the "Remove Not Assigned Media" section 3. The button shows the count of unused uncategorized media files 4. Click "Delete unused uncategorized media (N)" button (you will be asked to confirm) 5. Watch the progress bar as media files are being deleted in batches 6. Review the deletion statistics **What counts as "unused uncategorized media":** - Not assigned to any media category - Not used as a featured image for any post or product - Not used in any WooCommerce product image gallery - Not attached to any post (post_parent = 0) **Important Notes about Remove Not Assigned Media:** - **THIS OPERATION PERMANENTLY DELETES MEDIA FILES** that meet all criteria above - **The files will be removed from the server. THIS ACTION CANNOT BE UNDONE!** - Media files are processed in batches of 10 to prevent timeouts - Both the WordPress attachment post and the physical files (including thumbnails) are deleted - A real-time progress bar shows the deletion progress - The page will automatically reload after successful deletion to update the count #### Update Product Variant Prices (NEW in v1.5.3) 1. Go to Products > Category Manipulations 2. Scroll to the "Update product variant prices" section 3. Select one or more product categories using the checkboxes - Use "Check All" to select all categories - Use "Uncheck All" to deselect all categories 4. Pick a **Variant attribute** from the dropdown (heads such as Color, Size — populated from variation-eligible attributes in the store) 5. Pick a **Variant attribute value** from the dropdown (values are scoped to the chosen attribute and selected categories) 6. Enter the **New Price** 7. Watch the **Affected variations** label — it updates live as you change filters 8. Click **Dry Run** to preview the count and a sample of variations that would be updated (no DB writes) 9. Click **Apply** to commit the new regular price; you will be asked to confirm 10. Review the results showing updated / failed counts and any failed variation IDs **Important Notes about Update Product Variant Prices:** - The operation modifies only the `regular_price` of `product_variation` records - **Order line items, cart contents, and historical order data are never modified** — past orders keep their original prices - All updates go through the WooCommerce API (`set_regular_price` + `WC_Product_Variable::sync` on the parent variable products) - Parent variable products are re-synced after the batch so the storefront price-range cache stays correct - Always run **Dry Run** first to verify the affected count - If a variation belongs to a non-variable parent (orphan), it is counted as failed and listed in the result ## Sample CSVs ### Sample Product Category CSV ``` name,url-name,parent-name,description,display-type,visibility,protected-passwords,operation "Clothing","clothing","","Top quality clothing","default","public","","A" "Men","men","Clothing","Men's clothing","products","public","","A" "Women","women","Clothing","Women's clothing","products","protected","password123","A" "Kids","kids","Clothing","Kids clothing","products","public","","U" "Accessories","accessories","","All accessories","default","public","","D" ``` **Category Import Operations:** - `A` - Add a new product category - `U` - Update an existing product category (identified by name) - `D` - Delete an existing product category (identified by name) ### Sample Product CSV (NEW in v1.4) ``` ID,Typ,Katalogové číslo,Jméno,Nadřazené,Krátký popis,Popis,Název 1 vlastnosti,Hodnota(y) 1 vlastnosti,Kategorie,Obrázky,Běžná cena,Vlastnost 1 viditelnost,Vlastnost 1 globální,Vlastnost 1 varianta,Povolit zákaznické recenze?,Minimum Quantity,Maximum Quantity 0,variable,T-SHIRT-001,"Basic T-Shirt","","Comfortable cotton t-shirt","High-quality cotton t-shirt in various sizes",Size,"S|M|L|XL","Clothing|Men","https://example.com/tshirt.jpg","",1,1,1,1,1,10 0,variation,T-SHIRT-001-S,"Basic T-Shirt - S",T-SHIRT-001,"","",Size,S,"","https://example.com/tshirt-s.jpg",19.99,0,1,0,0,1,10 0,variation,T-SHIRT-001-M,"Basic T-Shirt - M",T-SHIRT-001,"","",Size,M,"","https://example.com/tshirt-m.jpg",19.99,0,1,0,0,1,10 ``` ## Menu Structure After activation, the plugin adds the following menu items under **Products** in the WordPress admin: ### Category Management - Import Product Categories - Export Product Categories - Category Manipulations - Move products between categories - Batch description between categories - Delete products in category - Clear Media Categories (NEW in v1.5) - Remove Not Assigned Media (NEW in v1.5.2) - Update product variant prices (NEW in v1.5.3) - Update product variant prices (NEW in v1.5.3) ### Product Management (NEW in v1.4) - **Product Import** - Batch import variable products and variations - **Product Export** - Export products by category selection ## License GPL v2 or later - https://www.gnu.org/licenses/gpl-2.0.html ## Author Dalibor Votruba Website: [Quadarax](https://www.quadarax.com) ## Support For support, please visit [https://www.quadarax.com/plugins/studiou-wc-product-cat-manage](https://www.quadarax.com/plugins/studiou-wc-product-cat-manage) ## Changelog ### Version 1.7.2 - **Product import: dedup now also catches images that were imported before v1.6.1.** The v1.6.1 dedup matched only attachments tagged with the `_studiou_wcpcm_source_url` postmeta — fine for anything imported via the dedup-aware code, but a re-import of a product whose image was already in the Media Library from an older import (or uploaded manually) would still sideload a duplicate (`-1.jpg`). The matcher now falls back to matching `_wp_attached_file` against the uploads-relative path derived from the URL (URL path minus the configured uploads baseurl path, URL-decoded). When the fallback hits, the matched attachment is backfilled with `_studiou_wcpcm_source_url` so the next import of the same URL takes the precise O(1) path. Order of precedence is unchanged: in-request static cache → `_studiou_wcpcm_source_url` exact match → fallback by uploads-relative path → fresh sideload. ### Version 1.7.1 - **Performance: category counts in the manipulation dropdowns are now a single aggregate query (M9).** Previously the manipulator looped over every product category and ran a separate `COUNT(DISTINCT)` query for each, which the 5-minute transient cache masked — but the cache is busted by every `set_object_terms` / `edited_term` / etc., which this plugin's own bulk operations fire constantly, so the per-category loop ran often. Replaced with one `GROUP BY` query that preserves the existing `post_type = 'product'` and `post_status IN (publish, private, draft)` filters via the `ON` clause, so trashed and pending products don't inflate the displayed counts. Zero-count categories still appear (LEFT JOIN). Cache layer unchanged. - **Product import: per-request cache for the full product_cat term list (M8).** The case-insensitive fallback inside `find_or_create_category_by_name` (still aliased as `find_category_by_name` for compat) used to call `get_terms()` on every miss-path row — O(N) categories per miss, O(N*M) overall. Now loaded once per AJAX request and reused; newly created terms append into the cache. Method renamed to make the "creates as a side effect" behaviour visible at call sites. - **Product import: success-row logs are now gated behind WP_DEBUG (M7).** "Processing categories for product X", "Categories assigned via set_category_ids()", "find_category_by_name called for Y", "Found by exact name match - ID Z", and similar success/info lines no longer flood the production PHP error log on every import. Failure-path logs (creation failed, term insert failed, image download failed, etc.) stay always-on. Matches the documented "Only logs failures" policy. - **Product export row count is now correct when fields contain newlines (L2).** The old `substr_count($csv_content, "\n") - 1` over-counted whenever a product description contained a newline. `export_products()` now accumulates the count during generation and returns it via a by-ref parameter. - **Runtime-generated versioned asset copies moved out of the plugin directory and into `uploads/studiou-wcpcm-cache/` (L4).** Generating versioned `admin-X.Y.Z.js` / `admin-X.Y.Z.css` files inside the plugin directory tripped some security scanners and failed on managed hosts with read-only plugin dirs. The new location is the writable WordPress uploads dir; the cache subdirectory gets `index.php` silence files. The plugin dir is left untouched. - **UPVP: detects and reports variations whose sale price ends up greater than the new regular price (L7).** When the operator sets a new regular price below an existing sale price (sale > regular is invalid), the operation now logs `STUDIOU WC UPVP: variation N has sale_price S greater than new regular_price R` per affected variation and includes a `sale_warnings` array in the AJAX response. The plugin does **not** auto-clamp the sale price — sale data is operator-owned — but the warning gives the operator the information needed to fix it. - **Security & operational notes documented in CLAUDE.md (L6, L8).** Server-side image fetching (SSRF surface gated by `manage_woocommerce`) and the failed-CSV retry being creation-only are both called out under a new "Security notes" section. - Two legacy single-shot AJAX actions from prior versions (`studiou_wcpcm_import`, `studiou_wcpcm_move_products`) remain registered for backwards compatibility. The bundled UI no longer calls them. ### Version 1.7.0 - **Category import is now batched and disk-staged (M1, M4 rework).** The legacy single-shot endpoint that processed the entire CSV in one AJAX call ran into the ~60 second PHP timeout on stores with thousands of categories. The new flow mirrors the product importer: the parse step stages rows to a guarded `wp-content/uploads/studiou-wcpcm-staging/.json` file (`index.php` + `.htaccess` guards, 24-hour TTL); the batch step processes 10 rows per AJAX call and reports progress; the JS controller polls until done. Two new AJAX actions: `studiou_wcpcm_import_parse`, `studiou_wcpcm_import_batch`. The legacy `studiou_wcpcm_import` action is still registered for backwards compatibility but is no longer used by the bundled UI. - **Product move is now batched (M2).** Same shape as the existing delete-products flow: a `studiou_wcpcm_move_products_start` action returns the source category's product IDs, then `studiou_wcpcm_move_products_chunk` moves 25 at a time. `wp_set_post_terms` is idempotent, so a retried chunk after a network blip won't corrupt anything. The single-shot `studiou_wcpcm_move_products` action remains for backwards compatibility. - **Product import no longer uses a WordPress transient to stage CSV rows (M4 rework).** The transient was a single `wp_options` row; large imports overflowed `max_allowed_packet` and surfaced as the misleading "Import data not found or expired". The new disk-streaming staging (shared with the category importer) has no such ceiling. The 1.6.2 transient-failure guard becomes unnecessary but remains as a defensive error path on the new disk write. - **Two new helper static methods on `Studiou_WC_Product_Manage_Product_Import`** (`get_staging_dir`, `stage_rows`, `read_stage_slice`, `delete_stage`, `prune_staging`) are the shared staging API used by both importers. ### Version 1.6.4 - **Security: gated product-export download.** Previously the product exporter dropped `product-export-YYYY-MM-DD-H-i-s.csv` into `wp-content/uploads/studiou-wc-product-exports/` and returned the raw public URL — anyone who knew or guessed the timestamped name could download the file, and the directory was never cleaned. Now mirrors the category exporter: - Export files live in `wp-content/uploads/studiou-wcpcm-product-exports/` with `index.php` and `.htaccess` guards. - Filenames embed a 32-character random hex token (`product-export-<32hex>.csv`) so the path is unguessable even if directory listing leaks. - Downloads now route through `admin-init` with a nonce check, a `manage_woocommerce` capability check, a strict regex on the requested basename, and a `realpath` confinement check that defends against path traversal and symlink escape. - Every new export prunes files older than 24 hours so the directory doesn't grow without bound. ### Version 1.6.3 - **Product import: correct round-trip for taxonomy and custom attributes.** The importer now reads the `Vlastnost 1 globální` column (previously written by the exporter but never consumed) and branches on it: - **Global / taxonomy attribute** (`globální = 1`): the importer registers the taxonomy, resolves each `Hodnota(y) 1 vlastnosti` value to its term (creating the term via `wp_insert_term` if it doesn't exist), passes the term IDs to `WC_Product_Attribute::set_options()`, and links the terms to the product via `wp_set_object_terms` after save. For variations the importer stores the term **slug** (not the raw name) under the `attribute_pa_*` meta key, so WooCommerce can match the customer's selection on the storefront. Previously the importer passed raw names to `set_options()` and stored raw names in variation meta, which meant taxonomy variations were unreachable ("No matching variation"). - **Custom (product-level) attribute** (`globální = 0` or column absent): the importer now correctly creates a non-taxonomy `WC_Product_Attribute` with `set_id(0)` and a plain display name, stores the raw values, and writes variation meta under `attribute_`. Previously every attribute was force-converted to a `pa_*` taxonomy, polluting the store with global taxonomies for what was meant to be a product-level attribute. - **Live-test gate before deployment.** Per the master plan §5.B.0, verify on a real WooCommerce install before shipping: create a variable product with a global `pa_*` attribute and 2-3 variations, export, delete, re-import the export, then storefront-test that each variation is selectable. The fix is shaped from the review's analysis; the live test is what proves it. ### Version 1.6.2 - **Safety: "Remove Not Assigned Media" no longer destroys WooCommerce product-category images.** The "unused" attachment query now also excludes any attachment referenced from termmeta `thumbnail_id` (the WC category-image convention) and the customizer (custom logo, site icon, header image). NOTE: this *narrows* the blast radius; it does not make the operation safe in general. Images embedded only in post / page / product content HTML, in ACF/meta fields, in custom termmeta keys other than `thumbnail_id`, and in WooCommerce downloadable-product files are still NOT detected and will still be deleted if you click the button. The tab warning has been rewritten to say so explicitly. The preview-list UI that actually closes the gap is a separate change tracked under review-00-plan-02. - **Product import: ragged CSV rows no longer crash the entire import.** On PHP 8+ `array_combine` throws `ValueError` (which extends `Error`, not `Exception`) when a row's column count doesn't match the header. The surrounding `catch (Exception)` missed it and the whole import died as a bare 500. Now: short rows are padded with empty cells (Excel trailing-comma artifact, recoverable); over-long rows are skipped with a clear "column count mismatch" reason and surface in the failed-CSV download. All product import / export handlers' catches widened from `Exception` to `\Throwable`. - **Product import: strip UTF-8 BOM on parse.** Excel commonly saves CSV with a leading BOM. Without this fix the first header became `"\xEF\xBB\xBFID"`, every row missed the `ID` key, every product was treated as new, and most then got skipped on SKU-uniqueness. Matches what the category importer has always done. For symmetry, the product export now also writes a BOM (Policy A: write BOM in exports, strip BOM on import) so an Excel round-trip is lossless. - **Product import: large CSVs that overflow the WordPress transient store now report a clear, actionable error** instead of the misleading downstream "Import data not found or expired". The structural fix that actually removes the size ceiling (disk-streaming staging) lands in 1.7.0. - **Category import update operation no longer silently re-parents children to root.** Previously, any `U` row with a blank `parent-name` would set `parent = 0`, which silently moved every child being updated up to root. Now: blank `parent-name` on a `U` row leaves the existing parent untouched. To explicitly move a child to root, set `parent-name` to the literal sentinel `__ROOT__`. `A` (add) rows keep blank = root, since a new category has no existing parent to preserve. - **Plugin header `WC requires at least` corrected from `9.0` to `9.8`** to match the runtime check and the documented requirement. ### Version 1.6.1 - **Product import: deduplicate media by source URL** - a single `Obrázky` URL is now downloaded **once** per import (and once per re-import), and every row that references it - the variable parent and all its variations - reuses the same attachment ID. Eliminates the `image-1.jpg`, `image-2.jpg`, ... clutter that previously appeared in `wp-content/uploads/`, and stops each variation from pointing at its own private copy of what is logically the parent's image. - New attachment postmeta `_studiou_wcpcm_source_url` records the trimmed source URL for every image this importer brings in, so dedup survives across batches and across re-imports months later. A per-request in-memory cache absorbs repeats within a single AJAX batch. - The on-disk filename is now derived from the URL path only (query string stripped), so `img.jpg?v=2` lands as `img.jpg`. The full URL (including query string) still keys the cache, so distinct URLs remain distinct attachments. - **Latent bug fixes in `upload_image_from_url()`**: - `intermediate_image_sizes_advanced` filter is now reliably removed - the previous code added and "removed" two separate closures, so `remove_filter()` never matched and the filter leaked into later media operations in the same request. Replaced with the stable `__return_empty_array` callable, detached in `finally` so it leaves on every exit path (success, error, exception). - Removed the redundant per-call `http_request_timeout` closure - it stacked one new closure per row (never removed) and was already covered by `download_url($url, 30)`. - Whitespace around image URLs is now trimmed at the upload entry point, matching what the validator already accepted. Two rows differing only by leading/trailing whitespace now collapse to one attachment instead of two. - No new translatable strings, no schema changes, no admin UI changes. The fix is entirely server-side in the product importer. ### Version 1.6.0 - **Category Manipulations page restructured into tabs** - Each of the 6 operations plus Important Notes is now a separate tab - A tab's HTML and its (expensive) data are fetched on demand via a separate AJAX request the first time the tab is opened - Fixes the page-load timeout on stores with many product categories or media files - the shell page no longer runs every category/media calculation up front - The active tab is preserved in the URL hash, so a refresh (and the automatic reload after move/delete/clear operations) returns to the same tab - New AJAX handler: `studiou_wcpcm_load_tab` renders a manipulations tab partial on demand - Category and media-category count queries are now transient-cached (5 minutes, automatically invalidated when categories or product/category relationships change) - 7 new view partials under `views/manipulations/` - **Cache busting** - admin CSS/JS are now served under version-stamped filenames (`admin-X.Y.Z.js` / `admin-X.Y.Z.css`), so a version bump forces browsers to fetch fresh assets even on hosts that strip the `?ver=` query string. No more manual hard refresh needed after an update. ### Version 1.5.3 - **NEW: Update product variant prices** - bulk-update regular price of product variations - Filter by selected product categories (with Check All / Uncheck All) - Variant attribute combobox (heads), populated from variation-eligible attributes - Variant value combobox, dynamically scoped to the chosen attribute and selected categories - New Price textbox with numeric validation - Dry Run button shows affected count + sample preview, no DB writes - Apply button commits via WooCommerce API (`set_regular_price` + `WC_Product_Variable::sync`) - Live affected-count label updates on every filter change - 4 new AJAX handlers: `studiou_wcpcm_upvp_get_attributes`, `studiou_wcpcm_upvp_get_attribute_values`, `studiou_wcpcm_upvp_count`, `studiou_wcpcm_upvp_apply` - Operation never touches order items, cart sessions, or historical order data - Added 23 new translation strings (English/Czech) ### Version 1.5.2 - **NEW: Remove Not Assigned Media** - permanently delete all unused uncategorized media files - Button with count of unused uncategorized media - Detects media not in any category, not used as featured image, not in product galleries, not attached to any post - Confirmation dialog before deletion - Real-time progress bar with batch processing (10 files at a time) - Comprehensive deletion statistics and error reporting - **Fix: Media deletion now properly deletes physical files and attachment posts** - After `wp_delete_attachment()`, verifies post was actually deleted via direct DB check - If post still exists, forces manual cleanup (term relationships, postmeta, post row) - Explicitly deletes physical files (main file + thumbnails) from disk - 2 new AJAX handlers: `studiou_wcpcm_remove_unassigned_media`, `studiou_wcpcm_remove_unassigned_media_batch` ### Version 1.5.1 - Fix: Clear Media Categories JS event handler not binding due to browser-cached old JS - Fix: Auto-detect any custom taxonomy registered for attachments (no longer limited to 3 hardcoded names) - Fix: Use spread operator for `$wpdb->prepare()` args for WP 6.8+ compatibility - Fix: Add `return` after `wp_send_json_error()` calls to prevent execution leak - Fix: Force `wp_update_term_count_now()` after batch deletion to refresh category counts - Improved JS initialization: each form init wrapped in try-catch (`safeInit`) so one error cannot block others - Always-on error logging for media operations (prefix: `STUDIOU WC MEDIA:`) - Added `onsubmit="return false;"` fallback on clear media form ### Version 1.5.0 - **NEW: Clear Media Categories** - permanently delete all media files from selected media categories - Checkbox list of media categories with file count per category - Check All / Uncheck All functionality - Confirmation dialog before deletion - Real-time progress bar for deletion operations - Batch processing for large-scale deletions (processes 10 media files at a time) - Comprehensive deletion statistics and error reporting - Enhanced warning messages for destructive operations - Auto-detection of media category taxonomy for attachments - Graceful fallback message when no media category plugin is installed - 2 new AJAX handlers: `studiou_wcpcm_clear_media`, `studiou_wcpcm_clear_media_batch` - Updated Category Manipulations page with new Clear Media Categories section - Added Important Notes entry for the new feature ### Version 1.4.0 - **NEW: Product Import** - Batch import variable products and variations from CSV - **Batch processing** - Processes 5 products per batch to avoid PHP timeout (60 sec limit) - **Auto-create categories** - Missing categories automatically created during import - Support for variable products with attributes - Support for product variations with parent references - Image URL handling with 30-second timeout, intermediate sizes disabled - Min/Max quantity support - 7 comprehensive import rules with validation - Failed products download for retry - Real-time progress bar showing "Processing: X / Y products" - Comprehensive error logging (always enabled, prefix: "STUDIOU WC IMPORT:") - Logs only skipped/failed items (not successful ones) - **NEW: Product Export** - Export products by category selection - Export variable products and their variations - **Full category paths** exported for subcategories (format: "Parent > Child") - Category hierarchy selection with product counts - Select All/Deselect All functionality - Full product data export (18 columns) - **Technical improvements**: - Two-phase import: Parse CSV to transient, then batch process - Smart category matching: exact name, slug, case-insensitive - Simple category lookup only (categories have unique names) - Image validation only checks format, not HTTP accessibility (prevents timeouts) - Added 3 new database/import/export classes for product management - Added 2 new view templates for product import/export - Enhanced JavaScript with recursive batch processing handlers - Added 71 new translation strings (English/Czech) - Updated plugin description to reflect product management capabilities - All operations use WooCommerce API (no direct SQL) ### Version 1.3.0 - **New feature: Delete products in category** - permanently delete all products and variants from selected categories - Real-time progress bar for deletion operations - Batch processing for large-scale deletions (processes 10 products at a time) - Automatic handling of variable products and their variations - Comprehensive deletion statistics and error reporting - Enhanced warning messages for destructive operations - Improved user interface with confirmation dialogs ### Version 1.2.0 - Added Category Manipulations functionality - New feature: Move products between categories - New feature: Batch description between categories - Enhanced admin interface with better user feedback - Improved security with proper nonce validation - Added comprehensive documentation - Direct database queries for reliable product detection ### Version 1.1.0 - Initial release with import/export functionality - Support for WooCommerce Protected Categories - CSV-based batch operations