77
88namespace Cloudinary \Media ;
99
10+ use Cloudinary \Media ;
11+ use WP_REST_Request ;
12+ use WP_REST_Response ;
13+
1014/**
1115 * Class WooCommerceGallery.
1216 *
@@ -20,16 +24,29 @@ class WooCommerceGallery {
2024 */
2125 private $ gallery ;
2226
27+ /**
28+ * The media instance.
29+ *
30+ * @var Media
31+ */
32+ private $ media ;
33+
2334 /**
2435 * Init woo gallery.
2536 *
2637 * @param Gallery $gallery Gallery instance.
38+ * @param Media $media Media instance.
2739 */
28- public function __construct ( Gallery $ gallery ) {
40+ public function __construct ( Gallery $ gallery, Media $ media ) {
2941 $ this ->gallery = $ gallery ;
42+ $ this ->media = $ media ;
43+
44+ if ( self ::woocommerce_active () ) {
45+ $ this ->setup_rest_hooks ();
3046
31- if ( self ::woocommerce_active () && $ this ->enabled () ) {
32- $ this ->setup_hooks ();
47+ if ( $ this ->enabled () ) {
48+ $ this ->setup_hooks ();
49+ }
3350 }
3451 }
3552
@@ -86,6 +103,13 @@ public function maybe_enqueue_scripts( $can ) {
86103 return $ can ;
87104 }
88105
106+ /**
107+ * Setup hooks for the REST API integration.
108+ */
109+ public function setup_rest_hooks () {
110+ add_filter ( 'rest_request_before_callbacks ' , array ( $ this , 'pre_process_product_images ' ), 10 , 3 );
111+ }
112+
89113 /**
90114 * Setup hooks for the gallery.
91115 */
@@ -105,4 +129,98 @@ static function () {
105129
106130 add_filter ( 'cloudinary_enqueue_gallery_script ' , array ( $ this , 'maybe_enqueue_scripts ' ) );
107131 }
132+
133+ /**
134+ * Pre-process product images in REST API requests to resolve Cloudinary URLs to existing
135+ * media library attachment IDs, preventing unnecessary sideloads and duplicate assets.
136+ *
137+ * @param WP_REST_Response|null $response The response object or null.
138+ * @param WP_REST_Server $handler The request handler.
139+ * @param WP_REST_Request $request The request object.
140+ *
141+ * @return WP_REST_Response|null
142+ */
143+ public function pre_process_product_images ( $ response , $ handler , $ request ) {
144+ $ route = $ request ->get_route ();
145+ $ method = $ request ->get_method ();
146+
147+ // Ignore requests to other API endpoints.
148+ if (
149+ false === strpos ( $ route , '/wc/ ' )
150+ || false === strpos ( $ route , '/products ' )
151+ || ! in_array ( $ method , array ( 'POST ' , 'PUT ' , 'PATCH ' ), true )
152+ ) {
153+ return $ response ;
154+ }
155+
156+ // We only care about requests that include images.
157+ $ images = $ request ->get_param ( 'images ' );
158+ if ( empty ( $ images ) || ! is_array ( $ images ) ) {
159+ return $ response ;
160+ }
161+
162+ $ modified = false ;
163+
164+ foreach ( $ images as $ index => $ image ) {
165+ // If the image ID is already passed, WooCommerce will be able to find the corresponding attachment from the Media Library.
166+ if ( ! empty ( $ image ['id ' ] ) ) {
167+ continue ;
168+ }
169+
170+ $ src = isset ( $ image ['src ' ] ) ? esc_url_raw ( $ image ['src ' ] ) : '' ;
171+
172+ // We only care about images with a cloudinary URL.
173+ if ( ! $ src || ! $ this ->media ->is_cloudinary_url ( $ src ) ) {
174+ continue ;
175+ }
176+
177+ $ attachment_id = $ this ->find_attachment_by_cloudinary_url ( $ src );
178+
179+ // Apply the ID so that WooCommerce assigns the existing attachment.
180+ if ( ! is_null ( $ attachment_id ) ) {
181+ $ images [ $ index ]['id ' ] = $ attachment_id ;
182+ $ modified = true ;
183+ }
184+ }
185+
186+ if ( $ modified ) {
187+ $ request ->set_param ( 'images ' , $ images );
188+ }
189+
190+ return $ response ;
191+ }
192+
193+ /**
194+ * Find an existing media library attachment that corresponds to a Cloudinary URL.
195+ *
196+ * The URL may include transformation segments, so the lookup proceeds in three steps:
197+ * exact sync key match, bare public ID match, then base key match.
198+ *
199+ * @param string $url A Cloudinary asset URL.
200+ *
201+ * @return int|null Attachment ID, or null if not found.
202+ */
203+ private function find_attachment_by_cloudinary_url ( $ url ) {
204+ // Step 1: exact sync key — handles URLs that already exist verbatim in the library.
205+ $ attachment_id = $ this ->media ->get_id_from_url ( $ url );
206+ if ( $ attachment_id ) {
207+ return $ attachment_id ;
208+ }
209+
210+ $ public_id = $ this ->media ->get_public_id_from_url ( $ url );
211+ if ( ! $ public_id ) {
212+ return null ;
213+ }
214+
215+ // Step 2: bare public ID — matches assets uploaded from WordPress without transformations.
216+ $ linked = $ this ->media ->get_linked_attachments ( $ public_id );
217+ if ( ! empty ( $ linked ) ) {
218+ return array_shift ( $ linked );
219+ }
220+
221+ // Step 3: base key — matches assets imported from Cloudinary.
222+ $ base_id = $ this ->media ->get_id_from_sync_key ( 'base_ ' . $ public_id );
223+
224+ return $ base_id ? $ base_id : null ;
225+ }
108226}
0 commit comments