@@ -186,28 +186,180 @@ public function test_client_side_processing_enabled_on_localhost() {
186186 }
187187
188188 /**
189- * This test must run in a separate process because the output buffer
190- * callback sends HTTP headers via header(), which would fail in the
191- * main PHPUnit process where output has already started.
189+ * Verifies that cross-origin elements get crossorigin="anonymous" added.
192190 *
193191 * @ticket 64766
194192 *
195193 * @runInSeparateProcess
196194 * @preserveGlobalState disabled
195+ *
196+ * @dataProvider data_elements_that_should_get_crossorigin
197+ *
198+ * @param string $html HTML input to process.
197199 */
198- public function test_output_buffer_adds_crossorigin_attributes ( ) {
200+ public function test_output_buffer_adds_crossorigin ( $ html ) {
199201 $ _SERVER ['HTTP_USER_AGENT ' ] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 ' ;
200202
201- // Start an outer buffer to capture the callback-processed output.
202203 ob_start ();
203204
204205 wp_start_cross_origin_isolation_output_buffer ();
205- echo ' <img src="https://external.example.com/image.jpg" /> ' ;
206+ echo $ html ;
206207
207- // Flush the inner buffer to trigger the callback, sending processed output to the outer buffer.
208208 ob_end_flush ();
209209 $ output = ob_get_clean ();
210210
211211 $ this ->assertStringContainsString ( 'crossorigin="anonymous" ' , $ output );
212212 }
213+
214+ /**
215+ * Data provider for elements that should receive crossorigin="anonymous".
216+ *
217+ * @return array[]
218+ */
219+ public function data_elements_that_should_get_crossorigin () {
220+ return array (
221+ 'cross-origin script ' => array (
222+ '<script src="https://external.example.com/script.js"></script> ' ,
223+ ),
224+ 'cross-origin audio ' => array (
225+ '<audio src="https://external.example.com/audio.mp3"></audio> ' ,
226+ ),
227+ 'cross-origin video ' => array (
228+ '<video src="https://external.example.com/video.mp4"></video> ' ,
229+ ),
230+ 'cross-origin link stylesheet ' => array (
231+ '<link rel="stylesheet" href="https://external.example.com/style.css" /> ' ,
232+ ),
233+ 'cross-origin source inside video ' => array (
234+ '<video><source src="https://external.example.com/video.mp4" type="video/mp4" /></video> ' ,
235+ ),
236+ );
237+ }
238+
239+ /**
240+ * Verifies that certain elements do not get crossorigin="anonymous" added.
241+ *
242+ * Images are excluded because under Document-Isolation-Policy:
243+ * isolate-and-credentialless, the browser handles cross-origin images
244+ * in credentialless mode without needing explicit CORS headers.
245+ *
246+ * @ticket 64766
247+ *
248+ * @runInSeparateProcess
249+ * @preserveGlobalState disabled
250+ *
251+ * @dataProvider data_elements_that_should_not_get_crossorigin
252+ *
253+ * @param string $html HTML input to process.
254+ */
255+ public function test_output_buffer_does_not_add_crossorigin ( $ html ) {
256+ $ _SERVER ['HTTP_USER_AGENT ' ] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 ' ;
257+
258+ ob_start ();
259+
260+ wp_start_cross_origin_isolation_output_buffer ();
261+ echo $ html ;
262+
263+ ob_end_flush ();
264+ $ output = ob_get_clean ();
265+
266+ $ this ->assertStringNotContainsString ( 'crossorigin="anonymous" ' , $ output );
267+ }
268+
269+ /**
270+ * Data provider for elements that should not receive crossorigin="anonymous".
271+ *
272+ * @return array[]
273+ */
274+ public function data_elements_that_should_not_get_crossorigin () {
275+ return array (
276+ 'cross-origin img ' => array (
277+ '<img src="https://external.example.com/image.jpg" /> ' ,
278+ ),
279+ 'cross-origin img with srcset ' => array (
280+ '<img src="https://external.example.com/image.jpg" srcset="https://external.example.com/image-2x.jpg 2x" /> ' ,
281+ ),
282+ 'link with cross-origin imagesrcset only ' => array (
283+ '<link rel="preload" as="image" imagesrcset="https://external.example.com/image.jpg 1x" href="/local-fallback.jpg" /> ' ,
284+ ),
285+ 'relative URL script ' => array (
286+ '<script src="/wp-includes/js/wp-embed.min.js"></script> ' ,
287+ ),
288+ );
289+ }
290+
291+ /**
292+ * Same-origin URLs should not get crossorigin="anonymous".
293+ *
294+ * Uses site_url() at runtime since the test domain varies by CI config.
295+ *
296+ * @ticket 64766
297+ *
298+ * @runInSeparateProcess
299+ * @preserveGlobalState disabled
300+ */
301+ public function test_output_buffer_does_not_add_crossorigin_to_same_origin () {
302+ $ _SERVER ['HTTP_USER_AGENT ' ] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 ' ;
303+
304+ ob_start ();
305+
306+ wp_start_cross_origin_isolation_output_buffer ();
307+ echo '<script src=" ' . site_url ( '/wp-includes/js/wp-embed.min.js ' ) . '"></script> ' ;
308+
309+ ob_end_flush ();
310+ $ output = ob_get_clean ();
311+
312+ $ this ->assertStringNotContainsString ( 'crossorigin="anonymous" ' , $ output );
313+ }
314+
315+ /**
316+ * Elements that already have a crossorigin attribute should not be modified.
317+ *
318+ * @ticket 64766
319+ *
320+ * @runInSeparateProcess
321+ * @preserveGlobalState disabled
322+ */
323+ public function test_output_buffer_does_not_override_existing_crossorigin () {
324+ $ _SERVER ['HTTP_USER_AGENT ' ] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 ' ;
325+
326+ ob_start ();
327+
328+ wp_start_cross_origin_isolation_output_buffer ();
329+ echo '<script src="https://external.example.com/script.js" crossorigin="use-credentials"></script> ' ;
330+
331+ ob_end_flush ();
332+ $ output = ob_get_clean ();
333+
334+ $ this ->assertStringContainsString ( 'crossorigin="use-credentials" ' , $ output , 'Existing crossorigin attribute should not be overridden. ' );
335+ $ this ->assertStringNotContainsString ( 'crossorigin="anonymous" ' , $ output );
336+ }
337+
338+ /**
339+ * Multiple tags in the same output should each be handled correctly.
340+ *
341+ * @ticket 64766
342+ *
343+ * @runInSeparateProcess
344+ * @preserveGlobalState disabled
345+ */
346+ public function test_output_buffer_handles_mixed_tags () {
347+ $ _SERVER ['HTTP_USER_AGENT ' ] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 ' ;
348+
349+ ob_start ();
350+
351+ wp_start_cross_origin_isolation_output_buffer ();
352+ echo '<img src="https://external.example.com/image.jpg" /> ' ;
353+ echo '<script src="https://external.example.com/script.js"></script> ' ;
354+ echo '<audio src="https://external.example.com/audio.mp3"></audio> ' ;
355+
356+ ob_end_flush ();
357+ $ output = ob_get_clean ();
358+
359+ // IMG should NOT have crossorigin.
360+ $ this ->assertStringContainsString ( '<img src="https://external.example.com/image.jpg" /> ' , $ output , 'IMG should not be modified. ' );
361+
362+ // Script and audio should have crossorigin.
363+ $ this ->assertSame ( 2 , substr_count ( $ output , 'crossorigin="anonymous" ' ), 'Script and audio should both get crossorigin, but not img. ' );
364+ }
213365}
0 commit comments