|
|
@@ -21,6 +21,14 @@ class Studiou_WC_Product_Manage_Product_Import {
|
|
|
*/
|
|
|
private $db;
|
|
|
|
|
|
+ /**
|
|
|
+ * In-request cache of trimmed source URL => attachment ID, so the same URL
|
|
|
+ * appearing multiple times within one AJAX batch costs at most one DB lookup.
|
|
|
+ *
|
|
|
+ * @var array<string,int>
|
|
|
+ */
|
|
|
+ private static $url_cache = array();
|
|
|
+
|
|
|
/**
|
|
|
* Constructor
|
|
|
*
|
|
|
@@ -456,57 +464,133 @@ class Studiou_WC_Product_Manage_Product_Import {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Upload image from URL
|
|
|
+ * Find an attachment previously imported from this exact 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.
|
|
|
+ *
|
|
|
+ * @param string $url Trimmed source URL.
|
|
|
+ * @return int Attachment ID, or 0 if none.
|
|
|
+ */
|
|
|
+ private function find_attachment_by_source_url($url) {
|
|
|
+ $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' => '_studiou_wcpcm_source_url',
|
|
|
+ 'value' => $url,
|
|
|
+ 'compare' => '=',
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ));
|
|
|
+
|
|
|
+ return !empty($ids) ? (int) $ids[0] : 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Upload an image from a URL, deduplicating by source URL.
|
|
|
*
|
|
|
- * @param string $url Image URL
|
|
|
- * @return int|null Attachment ID or null on failure
|
|
|
+ * A given source URL is downloaded at most once: an in-request static cache
|
|
|
+ * absorbs repeats within a batch, and a `_studiou_wcpcm_source_url` postmeta
|
|
|
+ * lookup reuses attachments across batches and across imports.
|
|
|
+ *
|
|
|
+ * @param string $url Image URL (raw value from the Obrázky column).
|
|
|
+ * @return int|null Attachment ID, or null on empty input / download failure.
|
|
|
*/
|
|
|
private function upload_image_from_url($url) {
|
|
|
+ // Normalize the key. process_product_row() validates a *trimmed* URL but
|
|
|
+ // the callers pass the raw cell, so trim here to keep the cache key, the
|
|
|
+ // DB lookup, the download, and the stored postmeta all identical to the
|
|
|
+ // value that was validated (and to collapse whitespace-only variants).
|
|
|
+ $url = is_string($url) ? trim($url) : '';
|
|
|
+ if ($url === '') {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Layer 2 — in-request static cache.
|
|
|
+ if (isset(self::$url_cache[$url])) {
|
|
|
+ if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
|
+ error_log('STUDIOU WC IMPORT: image cache HIT (request) ' . $url . ' -> ' . self::$url_cache[$url]);
|
|
|
+ }
|
|
|
+ return self::$url_cache[$url];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Layer 1 — persistent postmeta lookup (across batches and re-imports).
|
|
|
+ $existing = $this->find_attachment_by_source_url($url);
|
|
|
+ if ($existing) {
|
|
|
+ self::$url_cache[$url] = $existing;
|
|
|
+ if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
|
+ error_log('STUDIOU WC IMPORT: image cache HIT (db) ' . $url . ' -> ' . $existing);
|
|
|
+ }
|
|
|
+ return $existing;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Layer 0 — fresh sideload.
|
|
|
require_once(ABSPATH . 'wp-admin/includes/media.php');
|
|
|
require_once(ABSPATH . 'wp-admin/includes/file.php');
|
|
|
require_once(ABSPATH . 'wp-admin/includes/image.php');
|
|
|
|
|
|
- // Set shorter timeout for image download (30 seconds)
|
|
|
- add_filter('http_request_timeout', function() { return 30; });
|
|
|
+ // Suppress intermediate sizes for import speed. Use the stable
|
|
|
+ // '__return_empty_array' callable (not a fresh closure) so remove_filter()
|
|
|
+ // can actually match it, and detach it in finally so it can never leak
|
|
|
+ // into later media operations in this request.
|
|
|
+ add_filter('intermediate_image_sizes_advanced', '__return_empty_array');
|
|
|
|
|
|
try {
|
|
|
+ // download_url()'s second arg sets the request timeout (30s); no
|
|
|
+ // http_request_timeout filter is needed.
|
|
|
$tmp = download_url($url, 30);
|
|
|
-
|
|
|
if (is_wp_error($tmp)) {
|
|
|
- if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
|
- error_log('STUDIOU WC: Image download failed for URL: ' . $url . ' - ' . $tmp->get_error_message());
|
|
|
- }
|
|
|
+ error_log('STUDIOU WC IMPORT: image download failed for ' . $url . ' - ' . $tmp->get_error_message());
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
+ // Derive a clean on-disk filename. parse_url() strips any query string
|
|
|
+ // (img.jpg?v=2 -> img.jpg) so wp_unique_filename() doesn't fold the
|
|
|
+ // query into the basename. Null-safe: parse_url() may return null/false
|
|
|
+ // and basename(null) is deprecated on PHP 8.1+ (we require 8.2).
|
|
|
+ $path = parse_url($url, PHP_URL_PATH);
|
|
|
+ $name = (is_string($path) && $path !== '') ? basename($path) : '';
|
|
|
+ if ($name === '') {
|
|
|
+ $name = 'image-' . md5($url) . '.jpg';
|
|
|
+ }
|
|
|
+
|
|
|
$file_array = array(
|
|
|
- 'name' => basename($url),
|
|
|
- 'tmp_name' => $tmp
|
|
|
+ 'name' => $name,
|
|
|
+ 'tmp_name' => $tmp,
|
|
|
);
|
|
|
|
|
|
- // Disable image processing to speed up import
|
|
|
- add_filter('intermediate_image_sizes_advanced', function() { return array(); });
|
|
|
-
|
|
|
$id = media_handle_sideload($file_array, 0);
|
|
|
-
|
|
|
- // Re-enable image processing
|
|
|
- remove_filter('intermediate_image_sizes_advanced', function() { return array(); });
|
|
|
-
|
|
|
if (is_wp_error($id)) {
|
|
|
- @unlink($file_array['tmp_name']);
|
|
|
- if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
|
- error_log('STUDIOU WC: Image upload failed for URL: ' . $url . ' - ' . $id->get_error_message());
|
|
|
- }
|
|
|
+ @unlink($tmp);
|
|
|
+ error_log('STUDIOU WC IMPORT: image sideload failed for ' . $url . ' - ' . $id->get_error_message());
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- return $id;
|
|
|
+ // Tag the attachment so future rows / batches / imports dedup on it.
|
|
|
+ update_post_meta($id, '_studiou_wcpcm_source_url', $url);
|
|
|
|
|
|
- } catch (Exception $e) {
|
|
|
+ self::$url_cache[$url] = (int) $id;
|
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
|
- error_log('STUDIOU WC: Image upload exception for URL: ' . $url . ' - ' . $e->getMessage());
|
|
|
+ error_log('STUDIOU WC IMPORT: image sideloaded ' . $url . ' -> ' . $id);
|
|
|
}
|
|
|
+ return (int) $id;
|
|
|
+
|
|
|
+ } catch (Exception $e) {
|
|
|
+ error_log('STUDIOU WC IMPORT: image upload exception for ' . $url . ' - ' . $e->getMessage());
|
|
|
return null;
|
|
|
+ } finally {
|
|
|
+ remove_filter('intermediate_image_sizes_advanced', '__return_empty_array');
|
|
|
}
|
|
|
}
|
|
|
|