@@ -340,6 +340,175 @@ public function test_normalize_special_leading_newline_handling( string $input,
340340 $ this ->assertEqualHTML ( $ expected , $ normalized_twice );
341341 }
342342
343+ /**
344+ * Ensures that fuzzer-discovered inputs do not emit native PHP errors.
345+ *
346+ * @ticket 65372
347+ *
348+ * @dataProvider data_provider_fuzzer_native_error_cases
349+ *
350+ * @param string $input HTML input.
351+ * @param string|null $expected Expected normalized output, or null when unsupported.
352+ */
353+ public function test_normalize_fuzzer_cases_do_not_emit_native_errors ( string $ input , ?string $ expected ) {
354+ $ errors = array ();
355+
356+ /*
357+ * This test is checking for native PHP warnings/notices. Unsupported HTML may
358+ * intentionally cause wp_trigger_error() under WP_DEBUG, which is separate
359+ * from the native errors this regression test is trying to catch.
360+ */
361+ add_filter ( 'wp_trigger_error_trigger_error ' , '__return_false ' );
362+ set_error_handler (
363+ static function ( int $ errno , string $ errstr ) use ( &$ errors ) {
364+ $ errors [] = "{$ errno }: {$ errstr }" ;
365+ return true ;
366+ }
367+ );
368+
369+ try {
370+ $ normalized = WP_HTML_Processor::normalize ( $ input );
371+ } finally {
372+ restore_error_handler ();
373+ remove_filter ( 'wp_trigger_error_trigger_error ' , '__return_false ' );
374+ }
375+
376+ // Use assertSame() instead of assertEmpty() so PHPUnit shows captured error messages on failure.
377+ $ this ->assertSame ( array (), $ errors );
378+ $ this ->assertSame ( $ expected , $ normalized , 'Should have normalized the input. ' );
379+ }
380+
381+ /**
382+ * Data provider.
383+ *
384+ * @return array[]
385+ */
386+ public static function data_provider_fuzzer_native_error_cases () {
387+ return array (
388+ 'Unsupported active formatting ' => array ( '<A><I><A> ' , null ),
389+ );
390+ }
391+
392+ /**
393+ * Ensures that normalized fuzzer-discovered inputs remain supported.
394+ *
395+ * @ticket 65372
396+ *
397+ * @dataProvider data_provider_normalized_fuzzer_cases_that_should_remain_supported
398+ *
399+ * @param string $input HTML input.
400+ */
401+ public function test_normalized_fuzzer_cases_should_remain_supported ( string $ input ) {
402+ $ errors = array ();
403+ set_error_handler (
404+ static function ( int $ errno , string $ errstr ) use ( &$ errors ) {
405+ $ errors [] = "{$ errno }: {$ errstr }" ;
406+ return true ;
407+ }
408+ );
409+
410+ try {
411+ $ normalized = WP_HTML_Processor::normalize ( $ input );
412+ $ normalized_twice = is_string ( $ normalized ) ? WP_HTML_Processor::normalize ( $ normalized ) : null ;
413+ } finally {
414+ restore_error_handler ();
415+ }
416+
417+ // Use assertSame() instead of assertEmpty() so PHPUnit shows captured error messages on failure.
418+ $ this ->assertSame ( array (), $ errors );
419+ $ this ->assertIsString ( $ normalized , 'Input HTML should normalize successfully. ' );
420+ $ this ->assertIsString (
421+ $ normalized_twice ,
422+ 'Normalized HTML should remain supported by the HTML Processor. '
423+ );
424+ }
425+
426+ /**
427+ * Data provider.
428+ *
429+ * @return array[]
430+ */
431+ public static function data_provider_normalized_fuzzer_cases_that_should_remain_supported () {
432+ return array (
433+ 'Unclosed SVG TITLE after P in EM ' => array ( '<em><p><svg><title> ' ),
434+ 'Unclosed SVG TITLE after P in STRONG ' => array ( '<strong><p><svg ><title> ' ),
435+ );
436+ }
437+
438+ /**
439+ * Ensures that normalized fuzzer-discovered inputs are idempotent.
440+ *
441+ * @ticket 65372
442+ *
443+ * @dataProvider data_provider_normalized_fuzzer_cases_that_should_be_idempotent
444+ *
445+ * @param string $input HTML input.
446+ */
447+ public function test_normalized_fuzzer_cases_should_be_idempotent ( string $ input ) {
448+ $ errors = array ();
449+ set_error_handler (
450+ static function ( int $ errno , string $ errstr ) use ( &$ errors ) {
451+ $ errors [] = "{$ errno }: {$ errstr }" ;
452+ return true ;
453+ }
454+ );
455+
456+ try {
457+ $ normalized = WP_HTML_Processor::normalize ( $ input );
458+ $ normalized_twice = is_string ( $ normalized ) ? WP_HTML_Processor::normalize ( $ normalized ) : null ;
459+ } finally {
460+ restore_error_handler ();
461+ }
462+
463+ // Use assertSame() instead of assertEmpty() so PHPUnit shows captured error messages on failure.
464+ $ this ->assertSame ( array (), $ errors );
465+ $ this ->assertIsString ( $ normalized , 'Input HTML should normalize successfully. ' );
466+ $ this ->assertSame (
467+ $ normalized ,
468+ $ normalized_twice ,
469+ 'Normalizing already-normalized HTML should not change it. '
470+ );
471+ }
472+
473+ /**
474+ * Data provider.
475+ *
476+ * @return array[]
477+ */
478+ public static function data_provider_normalized_fuzzer_cases_that_should_be_idempotent () {
479+ return array (
480+ 'Malformed quoted attribute boundary ' => array ( '<A "/=> ' ),
481+ 'Duplicate attribute after bare attribute ' => array ( '<A V=5 R V=""=> ' ),
482+ 'Duplicate DATA-ID after numeric attribute ' => array ( '<E DATA-ID=1 1 DATA-ID=""=> ' ),
483+ 'Duplicate attribute before tag end ' => array ( '<R V=5 R V=5 => ' ),
484+ 'NULL byte in foreign tag name ' => array ( "<SVG><L \x00 D> " ),
485+ 'Malformed closing-looking attribute ' => array ( '<a </=> ' ),
486+ 'Malformed self-closing attribute ' => array ( '<a h/=> ' ),
487+ 'Duplicate ID with quote boundary ' => array ( '<d ID=""" ID=""=> ' ),
488+ 'Mixed-case duplicate TITLE ' => array ( "<d TITLE= \"\"' title= \"\"=> " ),
489+ 'Colon before self-closing slash ' => array ( '<e :/=> ' ),
490+ 'Duplicate class after bare attribute ' => array ( "<e class=y d class=''=> " ),
491+ 'Duplicate DATA-ID after hyphen ' => array ( '<e data-id=1 - data-id=""> ' ),
492+ 'Duplicate title after quotes ' => array ( "<e title=''' title= \"\"=> " ),
493+ 'FORM with SVG TITLE text edge ' => array ( "<form ><svg ><title \"'></form><form> " ),
494+ 'FORM with TABLE and SCRIPT ' => array ( '<form id><table te"><script></script><td srce" ID/></form><form claslicate"> ' ),
495+ 'FORM with TABLE CAPTION ' => array ( '<form><table><caption></form><form > ' ),
496+ 'Short malformed G attribute C ' => array ( '<g c/=> ' ),
497+ 'Short malformed G attribute S ' => array ( '<g s/=> ' ),
498+ 'Duplicate SRC boundary ' => array ( '<g src=""g src=""> ' ),
499+ 'Short malformed H attribute ' => array ( '<h f/=> ' ),
500+ 'Malformed SRC equals boundary ' => array ( '<i src=""= src=""="> ' ),
501+ 'Malformed slash in tag opener ' => array ( '<i/t/=> ' ),
502+ 'Malformed L colon attribute ' => array ( '<l :/=> ' ),
503+ 'Malformed L less-than attribute ' => array ( '<l/</=> ' ),
504+ 'Malformed N less-than attribute ' => array ( '<n </=> ' ),
505+ 'Unclosed SVG TITLE after P ' => array ( '<p><svg><title> ' ),
506+ 'Duplicate ALT boundary ' => array ( '<r alt= \'\'d alt=""=> ' ),
507+ 'NULL byte in SVG child tag ' => array ( "<svg><l \x00 '> " ),
508+ 'NULL byte before slash in SVG child tag ' => array ( "<svg><l \x00/r> " ),
509+ );
510+ }
511+
343512 /**
344513 * Data provider.
345514 *
0 commit comments