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