@@ -340,6 +340,180 @@ 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 {TICKET_NUMBER}
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 {TICKET_NUMBER}
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+ 'FORM in TABLE ' => array ( '<table><form> ' ),
434+ 'Mixed-case FORM in TABLE ' => array ( '<TABLE><Form> ' ),
435+ 'FORM in TABLE after SCRIPT ' => array ( '<table><script></script><form> ' ),
436+ 'Unclosed SVG TITLE after P in EM ' => array ( '<em><p><svg><title> ' ),
437+ 'Unclosed SVG TITLE after P in STRONG ' => array (
438+ '<strong><p><svg ><title> ' ,
439+ ),
440+ );
441+ }
442+
443+ /**
444+ * Ensures that normalized fuzzer-discovered inputs are idempotent.
445+ *
446+ * @ticket {TICKET_NUMBER}
447+ *
448+ * @dataProvider data_provider_normalized_fuzzer_cases_that_should_be_idempotent
449+ *
450+ * @param string $input HTML input.
451+ */
452+ public function test_normalized_fuzzer_cases_should_be_idempotent ( string $ input ) {
453+ $ errors = array ();
454+ set_error_handler (
455+ static function ( int $ errno , string $ errstr ) use ( &$ errors ) {
456+ $ errors [] = "{$ errno }: {$ errstr }" ;
457+ return true ;
458+ }
459+ );
460+
461+ try {
462+ $ normalized = WP_HTML_Processor::normalize ( $ input );
463+ $ normalized_twice = is_string ( $ normalized ) ? WP_HTML_Processor::normalize ( $ normalized ) : null ;
464+ } finally {
465+ restore_error_handler ();
466+ }
467+
468+ // Use assertSame() instead of assertEmpty() so PHPUnit shows captured error messages on failure.
469+ $ this ->assertSame ( array (), $ errors );
470+ $ this ->assertIsString ( $ normalized , 'Input HTML should normalize successfully. ' );
471+ $ this ->assertSame (
472+ $ normalized ,
473+ $ normalized_twice ,
474+ 'Normalizing already-normalized HTML should not change it. '
475+ );
476+ }
477+
478+ /**
479+ * Data provider.
480+ *
481+ * @return array[]
482+ */
483+ public static function data_provider_normalized_fuzzer_cases_that_should_be_idempotent () {
484+ return array (
485+ 'Malformed quoted attribute boundary ' => array ( '<A "/=> ' ),
486+ 'Duplicate attribute after bare attribute ' => array ( '<A V=5 R V=""=> ' ),
487+ 'Duplicate DATA-ID after numeric attribute ' => array ( '<E DATA-ID=1 1 DATA-ID=""=> ' ),
488+ 'Duplicate attribute before tag end ' => array ( '<R V=5 R V=5 => ' ),
489+ 'NULL byte in foreign tag name ' => array ( "<SVG><L \x00 D> " ),
490+ 'Malformed closing-looking attribute ' => array ( '<a </=> ' ),
491+ 'Malformed self-closing attribute ' => array ( '<a h/=> ' ),
492+ 'Duplicate ID with quote boundary ' => array ( '<d ID=""" ID=""=> ' ),
493+ 'Mixed-case duplicate TITLE ' => array ( "<d TITLE= \"\"' title= \"\"=> " ),
494+ 'Colon before self-closing slash ' => array ( '<e :/=> ' ),
495+ 'Duplicate class after bare attribute ' => array ( "<e class=y d class=''=> " ),
496+ 'Duplicate DATA-ID after hyphen ' => array ( '<e data-id=1 - data-id=""> ' ),
497+ 'Duplicate title after quotes ' => array ( "<e title=''' title= \"\"=> " ),
498+ 'FORM with SVG TITLE text edge ' => array ( "<form ><svg ><title \"'></form><form> " ),
499+ 'FORM with TABLE and SCRIPT ' => array ( '<form id><table te"><script></script><td srce" ID/></form><form claslicate"> ' ),
500+ 'FORM with TABLE CAPTION ' => array ( '<form><table><caption></form><form > ' ),
501+ 'Short malformed G attribute C ' => array ( '<g c/=> ' ),
502+ 'Short malformed G attribute S ' => array ( '<g s/=> ' ),
503+ 'Duplicate SRC boundary ' => array ( '<g src=""g src=""> ' ),
504+ 'Short malformed H attribute ' => array ( '<h f/=> ' ),
505+ 'Malformed SRC equals boundary ' => array ( '<i src=""= src=""="> ' ),
506+ 'Malformed slash in tag opener ' => array ( '<i/t/=> ' ),
507+ 'Malformed L colon attribute ' => array ( '<l :/=> ' ),
508+ 'Malformed L less-than attribute ' => array ( '<l/</=> ' ),
509+ 'Malformed N less-than attribute ' => array ( '<n </=> ' ),
510+ 'Unclosed SVG TITLE after P ' => array ( '<p><svg><title> ' ),
511+ 'Duplicate ALT boundary ' => array ( '<r alt= \'\'d alt=""=> ' ),
512+ 'NULL byte in SVG child tag ' => array ( "<svg><l \x00 '> " ),
513+ 'NULL byte before slash in SVG child tag ' => array ( "<svg><l \x00/r> " ),
514+ );
515+ }
516+
343517 /**
344518 * Data provider.
345519 *
0 commit comments