Pārlūkot izejas kodu

Catch pre-v1.6.1 attachments in image dedup — v1.7.2

Reported: re-importing a product whose image was already in the Media
Library (uploaded manually, or via a pre-v1.6.1 import) still sideloaded
a "18-IMG_3319-1.jpg" duplicate, because the existing attachment lacked
the _studiou_wcpcm_source_url postmeta the v1.6.1 matcher relied on.
This was a known limitation (fix-multi-image-plan-01.md §11) — closing
it now that real imports are hitting it.

Fix: extend find_attachment_by_source_url() with a fallback match by
_wp_attached_file against the uploads-relative path derived from the
URL. Steps:

1. Parse the URL's path (parse_url(..., PHP_URL_PATH)).
2. Parse the configured uploads baseurl's path (wp_upload_dir()).
3. Strip the baseurl path prefix from the URL path; urldecode the rest.
4. Query attachments where _wp_attached_file == that relative path.
5. On hit, write _studiou_wcpcm_source_url on the matched attachment so
   the next import of the same URL takes the precise O(1) path instead
   of the fallback. The cache is self-healing.

Order of precedence is unchanged: in-request static cache ->
_studiou_wcpcm_source_url exact match -> fallback by uploads-relative
path -> fresh sideload.

Trade-off note: two distinct URLs that resolve to the same uploads-
relative path collapse to the same attachment. That requires both URLs
to point at this WordPress install's own uploads tree — i.e. they
genuinely ARE the same file — so the fallback stays accurate in
practice.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Dalibor Votruba 1 mēnesi atpakaļ
vecāks
revīzija
e3464525a8

+ 1 - 1
studiou-wc-product-cat-manage/CLAUDE.md

@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
 
 **QDR - Studiou WC Export/Import Product Categories** is a WordPress plugin that extends WooCommerce to provide batch export/import functionality for both product categories and products, along with category manipulation tools.
 
-**Current Version:** 1.7.1
+**Current Version:** 1.7.2
 
 ## Requirements
 

+ 1 - 1
studiou-wc-product-cat-manage/assets/js/admin.js

@@ -19,7 +19,7 @@
     // Initialize admin scripts
     $(document).ready(function() {
         // Debug: Log that script is loaded
-        console.log('STUDIOU WC: Admin script loaded v1.7.1');
+        console.log('STUDIOU WC: Admin script loaded v1.7.2');
 
         if (typeof studiouWcpcm === 'undefined') {
             console.error('STUDIOU WC: studiouWcpcm object is NOT defined - AJAX will not work');

+ 78 - 6
studiou-wc-product-cat-manage/includes/class-studiou-wc-product-manage-product-import.php

@@ -764,12 +764,31 @@ class Studiou_WC_Product_Manage_Product_Import {
     }
 
     /**
-     * Find an attachment previously imported from this exact source URL.
+     * Find an attachment previously imported from this source URL.
      *
-     * Uses get_posts (not a raw $wpdb query) so a row whose attachment post was
-     * deleted but whose meta was somehow orphaned can't resolve to a missing post.
-     * Orders by ID ascending so the earliest (canonical) attachment wins
-     * deterministically if more than one ever carries the same source URL.
+     * v1.6.1 — primary match: `_studiou_wcpcm_source_url` postmeta exact ==
+     * the trimmed URL. This is the precise path; future re-imports of the
+     * same URL are O(1) DB hits.
+     *
+     * v1.7.2 — fallback match: for attachments that pre-date the v1.6.1
+     * dedup (so the source-URL postmeta was never written), match by the
+     * uploads-relative path stored in `_wp_attached_file`. The URL is
+     * parsed for its path, the configured uploads baseurl path is
+     * stripped, and what remains (e.g. `2024/03/18-IMG_3319.jpg`) is the
+     * value WordPress already stores in `_wp_attached_file`. Hits backfill
+     * `_studiou_wcpcm_source_url` on the matched attachment so the next
+     * import of the same URL takes the precise path.
+     *
+     * Trade-off: two distinct URLs that happen to resolve to the same
+     * uploads-relative path would collapse to one attachment. That can
+     * only happen if both URLs point at the same WordPress install's
+     * uploads tree — i.e. they really ARE the same file. The fallback
+     * therefore stays accurate.
+     *
+     * Uses get_posts (not a raw $wpdb query) so a row whose attachment post
+     * was deleted but whose meta was orphaned can't resolve to a missing
+     * post. Orders by ID ascending so the earliest (canonical) attachment
+     * wins deterministically.
      *
      * @param string $url Trimmed source URL.
      * @return int Attachment ID, or 0 if none.
@@ -794,7 +813,60 @@ class Studiou_WC_Product_Manage_Product_Import {
             ),
         ));
 
-        return !empty($ids) ? (int) $ids[0] : 0;
+        if (!empty($ids)) {
+            return (int) $ids[0];
+        }
+
+        // v1.7.2 — fallback by uploads-relative path.
+        $url_path = parse_url($url, PHP_URL_PATH);
+        if (!is_string($url_path) || $url_path === '') {
+            return 0;
+        }
+        $upload_dir = wp_upload_dir();
+        if (!empty($upload_dir['error']) || empty($upload_dir['baseurl'])) {
+            return 0;
+        }
+        $base_path = parse_url($upload_dir['baseurl'], PHP_URL_PATH);
+        if (!is_string($base_path) || $base_path === '' || strpos($url_path, $base_path) !== 0) {
+            return 0;
+        }
+        $rel = ltrim(substr($url_path, strlen($base_path)), '/');
+        if ($rel === '') {
+            return 0;
+        }
+        $rel = urldecode($rel); // CSV URLs may percent-encode spaces, etc.
+
+        $fallback_ids = get_posts(array(
+            'post_type'              => 'attachment',
+            'post_status'            => 'inherit',
+            'posts_per_page'         => 1,
+            'orderby'                => 'ID',
+            'order'                  => 'ASC',
+            'fields'                 => 'ids',
+            'no_found_rows'          => true,
+            'update_post_meta_cache' => false,
+            'update_post_term_cache' => false,
+            'meta_query'             => array(
+                array(
+                    'key'     => '_wp_attached_file',
+                    'value'   => $rel,
+                    'compare' => '=',
+                ),
+            ),
+        ));
+
+        if (empty($fallback_ids)) {
+            return 0;
+        }
+
+        $matched = (int) $fallback_ids[0];
+        // Backfill the source-URL postmeta so the next import of this URL
+        // hits the precise path instead of taking the fallback again.
+        update_post_meta($matched, '_studiou_wcpcm_source_url', $url);
+        if (defined('WP_DEBUG') && WP_DEBUG) {
+            error_log('STUDIOU WC IMPORT: image cache HIT (fallback by _wp_attached_file) ' . $url . ' -> ' . $matched . '; backfilled _studiou_wcpcm_source_url');
+        }
+        return $matched;
     }
 
     /**

+ 4 - 1
studiou-wc-product-cat-manage/readme.md

@@ -1,6 +1,6 @@
 # QDR - Studiou WC Export/Import Product Categories
 
-**Version: 1.7.1**
+**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.
 
@@ -352,6 +352,9 @@ For support, please visit [https://www.quadarax.com/plugins/studiou-wc-product-c
 
 ## 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 (`<name>-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.

+ 2 - 2
studiou-wc-product-cat-manage/studiou-wc-product-cat-manage.php

@@ -3,7 +3,7 @@
  * Plugin Name: QDR - Studiou WC Export/Import Product Categories
  * Plugin URI: https://www.quadarax.com/plugins/studiou-wc-product-cat-manage
  * Description: Allows batch import and export WooCommerce product categories and products
- * Version: 1.7.1
+ * Version: 1.7.2
  * Requires at least: 6.8.1
  * Requires PHP: 8.2
  * Author: Dalibor Votruba
@@ -23,7 +23,7 @@ if (!defined('WPINC')) {
 }
 
 // Define plugin constants
-if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.7.1');
+if (!defined('STUDIOU_WCPCM_VERSION')) define('STUDIOU_WCPCM_VERSION', '1.7.2');
 if (!defined('STUDIOU_WCPCM_PLUGIN_DIR')) define('STUDIOU_WCPCM_PLUGIN_DIR', plugin_dir_path(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_URL')) define('STUDIOU_WCPCM_PLUGIN_URL', plugin_dir_url(__FILE__));
 if (!defined('STUDIOU_WCPCM_PLUGIN_BASENAME')) define('STUDIOU_WCPCM_PLUGIN_BASENAME', plugin_basename(__FILE__));