44 *
55 * This covers the wp_get_attachment_image_src filter Cloudinary hooks so it keeps the last
66 * word on that specific, commonly-targeted core hook (see WPP-1183): it must bow out cheaply
7- * when a URL is already a Cloudinary URL or there is no image at all, correct a local URL when
8- * the attachment is actually deliverable and synced regardless of the caller's $icon argument
9- * (WP_Media_List_Table's list-mode thumbnail column passes icon=true for every attachment, real
10- * images included -- see wp-admin/includes/class-wp-media-list-table.php), and otherwise leave
11- * the result alone.
7+ * when a URL is already a Cloudinary URL or there is no image at all, only correct an attachment
8+ * that actually has a real image representation (an image, or a preview-capable format like PDF/
9+ * PSD -- anything else showing here with icon=true is a generic mime icon, not something to
10+ * replace with the raw asset URL), correct a local URL when the attachment is actually
11+ * deliverable and synced regardless of the caller's $icon argument (WP_Media_List_Table's
12+ * list-mode thumbnail column passes icon=true for every attachment, real images included -- see
13+ * wp-admin/includes/class-wp-media-list-table.php), and otherwise leave the result alone.
1214 *
1315 * cloudinary_id() and cloudinary_url() are stubbed out (Test_Attachment_Image_Src_Media below)
1416 * rather than driven through the real sync/signature pipeline, matching the approach in
@@ -54,45 +56,17 @@ protected function get_media() {
5456 return $ media ;
5557 }
5658
57- /**
58- * Utils::is_saving_metadata() reads did_action() counters (add_post_meta, etc.) that
59- * WordPress never resets between tests within one PHPUnit process -- any earlier test, or
60- * even our own wpSetUpBeforeClass() fixture, already ticks them past zero. Momentarily
61- * clearing them here isolates the checks that come after that guard in
62- * filter_attachment_image_src(), so those tests exercise their own branch instead of
63- * always bowing out at is_saving_metadata() for an unrelated, process-wide reason.
64- *
65- * @param callable $callback Callback to run with the guard cleared.
66- *
67- * @return mixed
68- */
69- protected function without_saving_metadata_guard ( callable $ callback ) {
70- $ keys = array ( 'add_post_meta ' , 'update_post_meta ' , 'add_term_meta ' , 'update_term_meta ' , 'add_user_meta ' , 'update_user_meta ' );
71- $ saved = array ();
72- foreach ( $ keys as $ key ) {
73- if ( isset ( $ GLOBALS ['wp_actions ' ][ $ key ] ) ) {
74- $ saved [ $ key ] = $ GLOBALS ['wp_actions ' ][ $ key ];
75- unset( $ GLOBALS ['wp_actions ' ][ $ key ] );
76- }
77- }
78- try {
79- return $ callback ();
80- } finally {
81- foreach ( $ saved as $ key => $ value ) {
82- $ GLOBALS ['wp_actions ' ][ $ key ] = $ value ;
83- }
84- }
85- }
86-
8759 /**
8860 * A URL that already lives on the configured Cloudinary domain is returned untouched -- the
8961 * cheap bow-out path the ticket calls for, taken before any sync/delivery check runs.
9062 *
9163 * @return void
9264 */
9365 public function test_already_cloudinary_url_is_returned_unchanged () {
94- $ media = $ this ->get_media ();
95- $ image = array ( 'https://res.cloudinary.com/test-cloud/image/upload/v1/sample.jpg ' , 100 , 100 , true );
66+ $ media = $ this ->get_media ();
67+ $ media ->stub_cloudinary_id = 'sample.jpg ' ;
68+ $ media ->stub_cloudinary_url = 'https://res.cloudinary.com/test-cloud/image/upload/should-not-be-used.jpg ' ;
69+ $ image = array ( 'https://res.cloudinary.com/test-cloud/image/upload/v1/sample.jpg ' , 100 , 100 , true );
9670
9771 $ result = $ media ->filter_attachment_image_src ( $ image , self ::$ attachment_id , 'thumbnail ' , false );
9872
@@ -115,11 +89,7 @@ public function test_icon_true_does_not_block_correcting_a_real_synced_image() {
11589 $ media ->stub_cloudinary_url = 'https://res.cloudinary.com/test-cloud/image/upload/sample.jpg ' ;
11690 $ image = array ( 'http://example.org/wp-content/uploads/canola.jpg ' , 100 , 100 , true );
11791
118- $ result = $ this ->without_saving_metadata_guard (
119- function () use ( $ media , $ image ) {
120- return $ media ->filter_attachment_image_src ( $ image , self ::$ attachment_id , 'thumbnail ' , true );
121- }
122- );
92+ $ result = $ media ->filter_attachment_image_src ( $ image , self ::$ attachment_id , 'thumbnail ' , true );
12393
12494 $ this ->assertSame ( $ media ->stub_cloudinary_url , $ result [0 ] );
12595 }
@@ -138,19 +108,71 @@ public function test_icon_fallback_is_unchanged_when_nothing_to_correct_it_to()
138108 'post_mime_type ' => 'image/jpeg ' ,
139109 )
140110 );
111+ // wp_attachment_is()/wp_attachment_is_image() return false unconditionally without a
112+ // real _wp_attached_file, regardless of post_mime_type -- see wp-includes/post.php.
113+ update_post_meta ( $ bare_id , '_wp_attached_file ' , 'bare.jpg ' );
141114
142115 $ media = $ this ->get_media ();
143116 $ image = array ( 'http://example.org/wp-includes/images/media/default.png ' , 48 , 64 , false );
144117
145- $ result = $ this ->without_saving_metadata_guard (
146- function () use ( $ media , $ image , $ bare_id ) {
147- return $ media ->filter_attachment_image_src ( $ image , $ bare_id , 'thumbnail ' , true );
148- }
118+ $ result = $ media ->filter_attachment_image_src ( $ image , $ bare_id , 'thumbnail ' , true );
119+
120+ $ this ->assertSame ( $ image , $ result );
121+ }
122+
123+ /**
124+ * A synced non-image, non-preview attachment (audio, zip, docx, ...) requested with icon=true
125+ * must keep its generic mime icon. Delivery::is_deliverable() treats every non-image,
126+ * non-video attachment as deliverable, so without a type check here, this would otherwise get
127+ * "corrected" to the raw asset's Cloudinary URL -- breaking the resulting <img> tag (caught in
128+ * PR review: WP_Media_List_Table's list view requests icon=true for every attachment type).
129+ *
130+ * @return void
131+ */
132+ public function test_synced_non_image_attachment_with_icon_is_returned_unchanged () {
133+ $ zip_id = self ::factory ()->post ->create (
134+ array (
135+ 'post_type ' => 'attachment ' ,
136+ 'post_mime_type ' => 'application/zip ' ,
137+ )
149138 );
139+ update_post_meta ( $ zip_id , '_wp_attached_file ' , 'archive.zip ' );
140+
141+ $ media = $ this ->get_media ();
142+ $ media ->stub_cloudinary_id = 'archive.zip ' ;
143+ $ media ->stub_cloudinary_url = 'https://res.cloudinary.com/test-cloud/raw/upload/archive.zip ' ;
144+ $ image = array ( 'http://example.org/wp-includes/images/media/archive.png ' , 48 , 64 , false );
145+
146+ $ result = $ media ->filter_attachment_image_src ( $ image , $ zip_id , 'thumbnail ' , true );
150147
151148 $ this ->assertSame ( $ image , $ result );
152149 }
153150
151+ /**
152+ * A synced preview-capable format (PDF/PSD) is the deliberate carve-out: it has a real image
153+ * preview to correct to, so icon=true must not block it either, mirroring the image case.
154+ *
155+ * @return void
156+ */
157+ public function test_synced_preview_only_attachment_with_icon_is_corrected () {
158+ $ pdf_id = self ::factory ()->post ->create (
159+ array (
160+ 'post_type ' => 'attachment ' ,
161+ 'post_mime_type ' => 'application/pdf ' ,
162+ )
163+ );
164+ update_post_meta ( $ pdf_id , '_wp_attached_file ' , 'document.pdf ' );
165+
166+ $ media = $ this ->get_media ();
167+ $ media ->stub_cloudinary_id = 'document.pdf ' ;
168+ $ media ->stub_cloudinary_url = 'https://res.cloudinary.com/test-cloud/image/upload/document.jpg ' ;
169+ $ image = array ( 'http://example.org/wp-includes/images/media/document.png ' , 48 , 64 , false );
170+
171+ $ result = $ media ->filter_attachment_image_src ( $ image , $ pdf_id , 'thumbnail ' , true );
172+
173+ $ this ->assertSame ( $ media ->stub_cloudinary_url , $ result [0 ] );
174+ }
175+
154176 /**
155177 * A false (no image) result has nothing to correct and must pass through unchanged.
156178 *
@@ -163,7 +185,9 @@ public function test_false_image_is_returned_unchanged() {
163185 }
164186
165187 /**
166- * A video attachment is left untouched, mirroring the same guard filter_downsize() uses.
188+ * A video attachment is left untouched -- caught by the image/preview-only type check before
189+ * ever reaching the deliverable/sync checks (a video has no image representation to correct
190+ * to here; its own URL is handled by attachment_url() instead).
167191 *
168192 * @return void
169193 */
@@ -174,15 +198,12 @@ public function test_video_attachment_is_returned_unchanged() {
174198 'post_mime_type ' => 'video/mp4 ' ,
175199 )
176200 );
201+ update_post_meta ( $ video_id , '_wp_attached_file ' , 'video.mp4 ' );
177202
178203 $ media = $ this ->get_media ();
179204 $ image = array ( 'http://example.org/wp-content/uploads/video.mp4 ' , 640 , 360 , true );
180205
181- $ result = $ this ->without_saving_metadata_guard (
182- function () use ( $ media , $ image , $ video_id ) {
183- return $ media ->filter_attachment_image_src ( $ image , $ video_id , 'thumbnail ' , false );
184- }
185- );
206+ $ result = $ media ->filter_attachment_image_src ( $ image , $ video_id , 'thumbnail ' , false );
186207
187208 $ this ->assertSame ( $ image , $ result );
188209 }
@@ -200,16 +221,14 @@ public function test_non_deliverable_attachment_is_returned_unchanged() {
200221 'post_mime_type ' => 'image/jpeg ' ,
201222 )
202223 );
224+ update_post_meta ( $ bare_id , '_wp_attached_file ' , 'bare.jpg ' );
203225
204- $ media = $ this ->get_media ();
205- $ media ->stub_cloudinary_id = 'sample.jpg ' ;
206- $ image = array ( 'http://example.org/wp-content/uploads/bare.jpg ' , 100 , 100 , true );
226+ $ media = $ this ->get_media ();
227+ $ media ->stub_cloudinary_id = 'sample.jpg ' ;
228+ $ media ->stub_cloudinary_url = 'https://res.cloudinary.com/test-cloud/image/upload/should-not-be-used.jpg ' ;
229+ $ image = array ( 'http://example.org/wp-content/uploads/bare.jpg ' , 100 , 100 , true );
207230
208- $ result = $ this ->without_saving_metadata_guard (
209- function () use ( $ media , $ image , $ bare_id ) {
210- return $ media ->filter_attachment_image_src ( $ image , $ bare_id , 'thumbnail ' , false );
211- }
212- );
231+ $ result = $ media ->filter_attachment_image_src ( $ image , $ bare_id , 'thumbnail ' , false );
213232
214233 $ this ->assertSame ( $ image , $ result );
215234 }
@@ -226,11 +245,7 @@ public function test_no_cloudinary_id_returns_image_unchanged() {
226245 $ media ->stub_cloudinary_url = 'https://res.cloudinary.com/test-cloud/image/upload/should-not-be-used.jpg ' ;
227246 $ image = array ( 'http://example.org/wp-content/uploads/canola.jpg ' , 100 , 100 , true );
228247
229- $ result = $ this ->without_saving_metadata_guard (
230- function () use ( $ media , $ image ) {
231- return $ media ->filter_attachment_image_src ( $ image , self ::$ attachment_id , 'thumbnail ' , false );
232- }
233- );
248+ $ result = $ media ->filter_attachment_image_src ( $ image , self ::$ attachment_id , 'thumbnail ' , false );
234249
235250 $ this ->assertSame ( $ image , $ result );
236251 }
@@ -247,17 +262,38 @@ public function test_local_url_is_corrected_to_cloudinary_url() {
247262 $ media ->stub_cloudinary_url = 'https://res.cloudinary.com/test-cloud/image/upload/sample.jpg ' ;
248263 $ image = array ( 'http://example.org/wp-content/uploads/canola.jpg ' , 100 , 100 , true );
249264
250- $ result = $ this ->without_saving_metadata_guard (
251- function () use ( $ media , $ image ) {
252- return $ media ->filter_attachment_image_src ( $ image , self ::$ attachment_id , 'thumbnail ' , false );
253- }
254- );
265+ $ result = $ media ->filter_attachment_image_src ( $ image , self ::$ attachment_id , 'thumbnail ' , false );
255266
256267 $ this ->assertSame ( $ media ->stub_cloudinary_url , $ result [0 ] );
257268 $ this ->assertSame ( 100 , $ result [1 ] );
258269 $ this ->assertSame ( 100 , $ result [2 ] );
259270 $ this ->assertTrue ( $ result [3 ] );
260271 }
272+
273+ /**
274+ * Regression test for a PR review finding: Utils::is_saving_metadata() must reflect a meta
275+ * write happening right now, not one that happened earlier in the request. It used to read
276+ * did_action(), which is cumulative for the whole request -- so an unrelated post/term/user
277+ * meta write anywhere earlier (a view counter, a session plugin, anything) would permanently
278+ * block correction for the rest of the page once these filters started running on the front
279+ * end. Writing unrelated meta here, before calling the filter, catches a regression back to
280+ * that behaviour.
281+ *
282+ * @return void
283+ */
284+ public function test_earlier_unrelated_meta_write_does_not_block_correction () {
285+ $ other_post_id = self ::factory ()->post ->create ();
286+ update_post_meta ( $ other_post_id , 'unrelated_counter ' , 1 );
287+
288+ $ media = $ this ->get_media ();
289+ $ media ->stub_cloudinary_id = 'sample.jpg ' ;
290+ $ media ->stub_cloudinary_url = 'https://res.cloudinary.com/test-cloud/image/upload/sample.jpg ' ;
291+ $ image = array ( 'http://example.org/wp-content/uploads/canola.jpg ' , 100 , 100 , true );
292+
293+ $ result = $ media ->filter_attachment_image_src ( $ image , self ::$ attachment_id , 'thumbnail ' , false );
294+
295+ $ this ->assertSame ( $ media ->stub_cloudinary_url , $ result [0 ] );
296+ }
261297}
262298
263299/**
0 commit comments