From 11a9a021ebf4d425a4541b06f19b3e195510173e Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Fri, 24 Apr 2026 13:45:16 +0200 Subject: [PATCH 1/2] Avoid duplicated images when sideloading images to Woo API --- php/class-media.php | 2 +- php/media/class-woocommercegallery.php | 124 ++++++++++++++++++++++++- 2 files changed, 122 insertions(+), 4 deletions(-) diff --git a/php/class-media.php b/php/class-media.php index 8aad676d2..2bd521279 100644 --- a/php/class-media.php +++ b/php/class-media.php @@ -3147,7 +3147,7 @@ public function setup() { // Internal components. $this->global_transformations = new Global_Transformations( $this ); $this->gallery = $this->plugin->get_component( 'gallery' ); - $this->woocommerce_gallery = new WooCommerceGallery( $this->gallery ); + $this->woocommerce_gallery = new WooCommerceGallery( $this->gallery, $this ); $this->filter = new Filter( $this ); $this->upgrade = new Upgrade( $this ); $this->video = new Video( $this ); diff --git a/php/media/class-woocommercegallery.php b/php/media/class-woocommercegallery.php index 5b06bb5de..b64229004 100644 --- a/php/media/class-woocommercegallery.php +++ b/php/media/class-woocommercegallery.php @@ -7,6 +7,10 @@ namespace Cloudinary\Media; +use Cloudinary\Media; +use WP_REST_Request; +use WP_REST_Response; + /** * Class WooCommerceGallery. * @@ -20,16 +24,29 @@ class WooCommerceGallery { */ private $gallery; + /** + * The media instance. + * + * @var Media + */ + private $media; + /** * Init woo gallery. * * @param Gallery $gallery Gallery instance. + * @param Media $media Media instance. */ - public function __construct( Gallery $gallery ) { + public function __construct( Gallery $gallery, Media $media ) { $this->gallery = $gallery; + $this->media = $media; + + if ( self::woocommerce_active() ) { + $this->setup_rest_hooks(); - if ( self::woocommerce_active() && $this->enabled() ) { - $this->setup_hooks(); + if ( $this->enabled() ) { + $this->setup_hooks(); + } } } @@ -86,6 +103,13 @@ public function maybe_enqueue_scripts( $can ) { return $can; } + /** + * Setup hooks for the REST API integration. + */ + public function setup_rest_hooks() { + add_filter( 'rest_request_before_callbacks', array( $this, 'pre_process_product_images' ), 10, 3 ); + } + /** * Setup hooks for the gallery. */ @@ -105,4 +129,98 @@ static function () { add_filter( 'cloudinary_enqueue_gallery_script', array( $this, 'maybe_enqueue_scripts' ) ); } + + /** + * Pre-process product images in REST API requests to resolve Cloudinary URLs to existing + * media library attachment IDs, preventing unnecessary sideloads and duplicate assets. + * + * @param WP_REST_Response|null $response The response object or null. + * @param WP_REST_Server $handler The request handler. + * @param WP_REST_Request $request The request object. + * + * @return WP_REST_Response|null + */ + public function pre_process_product_images( $response, $handler, $request ) { + $route = $request->get_route(); + $method = $request->get_method(); + + // Ignore requests to other API endpoints. + if ( + false === strpos( $route, '/wc/' ) + || false === strpos( $route, '/products' ) + || ! in_array( $method, array( 'POST', 'PUT', 'PATCH' ), true ) + ) { + return $response; + } + + // We only care about requests that include images. + $images = $request->get_param( 'images' ); + if ( empty( $images ) || ! is_array( $images ) ) { + return $response; + } + + $modified = false; + + foreach ( $images as $index => $image ) { + // If the image ID is already passed, WooCommerce will be able to find the corresponding attachment from the Media Library. + if ( ! empty( $image['id'] ) ) { + continue; + } + + $src = isset( $image['src'] ) ? esc_url_raw( $image['src'] ) : ''; + + // We only care about images with a cloudinary URL. + if ( ! $src || ! $this->media->is_cloudinary_url( $src ) ) { + continue; + } + + $attachment_id = $this->find_attachment_by_cloudinary_url( $src ); + + // Apply the ID so that WooCommerce assigns the existing attachment. + if ( ! is_null( $attachment_id ) ) { + $images[ $index ]['id'] = $attachment_id; + $modified = true; + } + } + + if ( $modified ) { + $request->set_param( 'images', $images ); + } + + return $response; + } + + /** + * Find an existing media library attachment that corresponds to a Cloudinary URL. + * + * The URL may include transformation segments, so the lookup proceeds in three steps: + * exact sync key match, bare public ID match, then base key match. + * + * @param string $url A Cloudinary asset URL. + * + * @return int|null Attachment ID, or null if not found. + */ + private function find_attachment_by_cloudinary_url( $url ) { + // Step 1: exact sync key — handles URLs that already exist verbatim in the library. + $attachment_id = $this->media->get_id_from_url( $url ); + if ( $attachment_id ) { + return $attachment_id; + } + + $public_id = $this->media->get_public_id_from_url( $url ); + if ( ! $public_id ) { + return null; + } + + // Step 2: bare public ID — matches assets uploaded from WordPress without transformations. + $linked = $this->media->get_linked_attachments( $public_id ); + if ( ! empty( $linked ) ) { + return array_shift( $linked ); + } + + // Step 3: base key — matches assets imported from Cloudinary. + $base_id = $this->media->get_id_from_sync_key( 'base_' . $public_id ); + + return $base_id ? $base_id : null; + } } From a7c6269b50ecd131e1716900483a77adeae06bae Mon Sep 17 00:00:00 2001 From: Gabriel de Tassigny Date: Mon, 27 Apr 2026 08:50:37 +0200 Subject: [PATCH 2/2] Add missing use statement --- php/media/class-woocommercegallery.php | 1 + 1 file changed, 1 insertion(+) diff --git a/php/media/class-woocommercegallery.php b/php/media/class-woocommercegallery.php index b64229004..33efba7d4 100644 --- a/php/media/class-woocommercegallery.php +++ b/php/media/class-woocommercegallery.php @@ -10,6 +10,7 @@ use Cloudinary\Media; use WP_REST_Request; use WP_REST_Response; +use WP_REST_Server; /** * Class WooCommerceGallery.