@@ -78,15 +78,59 @@ public static function prepare_overlay( array $plan, array $resolved_plan ) {
7878 }
7979
8080 $ font_faces = self ::resolve_google_font_faces ( $ plan , $ families , $ diagnostics );
81- if ( '' === $ font_faces ) {
81+ if ( 'preserved ' === $ font_faces ['state ' ] ) {
82+ $ preserved_url = (string ) $ font_faces ['url ' ];
83+ $ fallback_url = self ::is_google_stylesheet_url ( $ preserved_url ) ? $ preserved_url : '' ;
84+ $ imports = array ();
85+ foreach ( $ plan ['stylesheets ' ] ?? array () as $ stylesheet ) {
86+ $ content = is_array ( $ stylesheet ) && is_scalar ( $ stylesheet ['content ' ] ?? null ) ? (string ) $ stylesheet ['content ' ] : '' ;
87+ if ( preg_match_all ( '/@import\s+(?:url\()?\s*[" \']?([^" \'\s\)]+)[" \']?\s*\)?/i ' , $ content , $ matches ) ) {
88+ foreach ( $ matches [1 ] as $ candidate ) {
89+ if ( self ::is_google_stylesheet_url ( $ candidate ) ) {
90+ $ imports [] = $ candidate ;
91+ }
92+ }
93+ }
94+ }
95+ $ imports = array_values ( array_unique ( $ imports ) );
96+ if ( '' === $ fallback_url && ! empty ( $ imports ) ) {
97+ $ fallback_url = $ imports [0 ];
98+ }
99+ $ css_lines = array ();
100+ foreach ( $ imports as $ import_url ) {
101+ $ css_lines [] = '@import " ' . addcslashes ( $ import_url , "\"\\\n" ) . '"; ' ;
102+ }
103+ if ( empty ( $ css_lines ) && '' !== $ fallback_url ) {
104+ $ css_lines [] = '@import " ' . addcslashes ( $ fallback_url , "\"\\\n" ) . '"; ' ;
105+ }
106+ $ css_body = empty ( $ css_lines ) ? '' : trim ( implode ( "\n" , $ css_lines ) ) . "\n" ;
107+ $ outer_detail = array (
108+ 'reason ' => (string ) $ font_faces ['reason ' ],
109+ 'observed_bytes ' => (int ) $ font_faces ['observed_bytes ' ],
110+ 'url ' => $ preserved_url ,
111+ );
112+ if ( 'google_fonts_payloads_partial_preserved ' === $ outer_detail ['reason ' ] ) {
113+ $ outer_detail ['limit_bytes ' ] = self ::TOTAL_FONT_LIMIT ;
114+ $ outer_detail ['aggregate_bytes ' ] = (int ) ( $ font_faces ['aggregate_bytes ' ] ?? $ font_faces ['observed_bytes ' ] );
115+ } elseif ( 'google_fonts_stylesheet_preserved_due_to_size ' === $ outer_detail ['reason ' ] ) {
116+ $ outer_detail ['limit_bytes ' ] = self ::CSS_LIMIT ;
117+ }
118+ $ diagnostics [] = self ::diagnostic_with_detail ( 'font_materialization_partial_preserved ' , $ outer_detail );
119+ if ( '' === $ css_body ) {
120+ return new WP_Error ( 'static_site_importer_font_materialization_failed ' , '' , $ diagnostics );
121+ }
122+ $ writes [] = self ::write ( 'assets/css/embedded-fonts.css ' , $ css_body , 'theme.font_materialization ' );
123+ return self ::with_runtime_registration ( $ writes , $ resolved_plan , array (), $ diagnostics );
124+ }
125+ $ embedded_css = (string ) $ font_faces ['css ' ];
126+ if ( '' === trim ( $ embedded_css ) ) {
82127 return new WP_Error ( 'static_site_importer_font_materialization_failed ' , '' , $ diagnostics );
83128 }
84-
85- $ writes [] = self ::write ( 'assets/css/embedded-fonts.css ' , trim ( $ font_faces ) . "\n" , 'theme.font_materialization ' );
129+ $ writes [] = self ::write ( 'assets/css/embedded-fonts.css ' , trim ( $ embedded_css ) . "\n" , 'theme.font_materialization ' );
86130 foreach ( $ svg_writes as $ svg_write ) {
87131 $ writes [] = self ::write (
88132 $ svg_write ['target_path ' ],
89- self ::embed_svg_font_faces ( $ svg_write ['content ' ], $ font_faces ),
133+ self ::embed_svg_font_faces ( $ svg_write ['content ' ], $ embedded_css ),
90134 $ svg_write ['source_path ' ]
91135 );
92136 }
@@ -532,8 +576,11 @@ private static function matching_svg_writes( array $writes, array $families ): a
532576 return $ matches ;
533577 }
534578
535- /** @param array<int,string> $families @param array<int,array<string,string>> $diagnostics */
536- private static function resolve_google_font_faces ( array $ plan , array $ families , array &$ diagnostics ): string {
579+ /** @param array<int,string> $families @param array<int,array<string,string>> $diagnostics
580+ * @return array{state:'embedded',css:string}
581+ * | array{state:'preserved',reason:string,observed_bytes:int,url:string,aggregate_bytes?:int}
582+ */
583+ private static function resolve_google_font_faces ( array $ plan , array $ families , array &$ diagnostics ): array {
537584 $ imports = array ();
538585 foreach ( $ plan ['stylesheets ' ] ?? array () as $ stylesheet ) {
539586 $ content = is_array ( $ stylesheet ) && is_scalar ( $ stylesheet ['content ' ] ?? null ) ? (string ) $ stylesheet ['content ' ] : '' ;
@@ -544,12 +591,22 @@ private static function resolve_google_font_faces( array $plan, array $families,
544591 $ imports = array_values ( array_unique ( $ imports ) );
545592 if ( empty ( $ imports ) ) {
546593 $ diagnostics [] = self ::diagnostic ( 'stylesheet_import_missing ' );
547- return '' ;
594+ return array (
595+ 'state ' => 'preserved ' ,
596+ 'reason ' => 'stylesheet_import_missing ' ,
597+ 'observed_bytes ' => 0 ,
598+ 'url ' => '' ,
599+ );
548600 }
549601 foreach ( $ imports as $ import ) {
550602 if ( ! self ::is_google_stylesheet_url ( $ import ) ) {
551603 $ diagnostics [] = self ::diagnostic ( 'untrusted_stylesheet_url ' );
552- return '' ;
604+ return array (
605+ 'state ' => 'preserved ' ,
606+ 'reason ' => 'untrusted_stylesheet_url ' ,
607+ 'observed_bytes ' => 0 ,
608+ 'url ' => (string ) $ import ,
609+ );
553610 }
554611 }
555612
@@ -560,58 +617,142 @@ private static function resolve_google_font_faces( array $plan, array $families,
560617 $ response = self ::request ( $ import , self ::CSS_LIMIT );
561618 $ css = is_wp_error ( $ response ) ? '' : (string ) wp_remote_retrieve_body ( $ response );
562619 if ( is_wp_error ( $ response ) || 200 !== (int ) wp_remote_retrieve_response_code ( $ response ) || '' === $ css ) {
563- $ diagnostics [] = self ::diagnostic ( 'stylesheet_fetch_failed ' );
564- return '' ;
620+ $ diagnostics [] = self ::diagnostic_with_detail (
621+ 'stylesheet_fetch_failed ' ,
622+ array (
623+ 'url ' => $ import ,
624+ 'observed_bytes ' => strlen ( $ css ),
625+ 'limit_bytes ' => self ::CSS_LIMIT ,
626+ )
627+ );
628+ return array (
629+ 'state ' => 'preserved ' ,
630+ 'reason ' => 'stylesheet_fetch_failed ' ,
631+ 'observed_bytes ' => strlen ( $ css ),
632+ 'url ' => $ import ,
633+ );
565634 }
566635 if ( strlen ( $ css ) > self ::CSS_LIMIT ) {
567- $ diagnostics [] = self ::diagnostic ( 'stylesheet_response_too_large ' );
568- return '' ;
636+ $ diagnostics [] = self ::diagnostic_with_detail (
637+ 'google_fonts_stylesheet_preserved_due_to_size ' ,
638+ array (
639+ 'url ' => $ import ,
640+ 'observed_bytes ' => strlen ( $ css ),
641+ 'limit_bytes ' => self ::CSS_LIMIT ,
642+ )
643+ );
644+ return array (
645+ 'state ' => 'preserved ' ,
646+ 'reason ' => 'google_fonts_stylesheet_preserved_due_to_size ' ,
647+ 'observed_bytes ' => strlen ( $ css ),
648+ 'url ' => $ import ,
649+ );
569650 }
570651 $ embedded = self ::embed_font_sources ( $ css , $ families , $ font_payloads , $ font_payload_bytes , $ diagnostics );
571- if ( '' === $ embedded ) {
572- return '' ;
652+ if ( 'preserved ' === $ embedded ['state ' ] ) {
653+ if ( '' === (string ) $ embedded ['url ' ] ) {
654+ $ embedded ['url ' ] = $ import ;
655+ }
656+ return $ embedded ;
573657 }
574- $ faces [] = $ embedded ;
658+ $ faces [] = ( string ) $ embedded[ ' css ' ] ;
575659 }
576- return implode ( "\n" , $ faces );
660+ return array (
661+ 'state ' => 'embedded ' ,
662+ 'css ' => implode ( "\n" , $ faces ),
663+ );
577664 }
578665
579- /** @param array<int,string> $families @param array<string,string> $payloads @param array<int,array<string,string>> $diagnostics */
580- private static function embed_font_sources ( string $ css , array $ families , array &$ payloads , int &$ payload_bytes , array &$ diagnostics ): string {
666+ /** @param array<int,string> $families @param array<string,string> $payloads @param array<int,array<string,string>> $diagnostics
667+ * @return array{state:'embedded',css:string}
668+ * | array{state:'preserved',reason:string,observed_bytes:int,url:string,aggregate_bytes?:int}
669+ */
670+ private static function embed_font_sources ( string $ css , array $ families , array &$ payloads , int &$ payload_bytes , array &$ diagnostics ): array {
581671 if ( ! preg_match_all ( '/@font-face\s*\{([^{}]*)\}/is ' , $ css , $ faces ) ) {
582- $ diagnostics [] = self ::diagnostic ( 'stylesheet_font_faces_missing ' );
583- return '' ;
672+ $ diagnostics [] = self ::diagnostic_with_detail (
673+ 'stylesheet_font_faces_missing ' ,
674+ array (
675+ 'observed_bytes ' => strlen ( $ css ),
676+ 'limit_bytes ' => self ::CSS_LIMIT ,
677+ )
678+ );
679+ return array (
680+ 'state ' => 'preserved ' ,
681+ 'reason ' => 'stylesheet_font_faces_missing ' ,
682+ 'observed_bytes ' => strlen ( $ css ),
683+ 'url ' => '' ,
684+ );
584685 }
585- $ embedded = array ();
686+ $ embedded = array ();
687+ $ current_source_url = '' ;
586688 foreach ( $ faces [0 ] as $ index => $ face ) {
689+ $ current_source_url = '' ;
587690 if ( ! preg_match ( '/font-family\s*:\s*([" \']?)([^;" \']+)\1\s*;/i ' , $ faces [1 ][ $ index ], $ family ) || ! in_array ( trim ( $ family [2 ] ), $ families , true ) ) {
588691 continue ;
589692 }
590693 if ( str_contains ( $ face , '< ' ) || ! preg_match_all ( '/url\(\s*([" \']?)([^" \'\s\)]+)\1\s*\)/i ' , $ face , $ urls ) ) {
591694 $ diagnostics [] = self ::diagnostic ( 'untrusted_font_url ' );
592- return '' ;
695+ return array (
696+ 'state ' => 'preserved ' ,
697+ 'reason ' => 'untrusted_font_url ' ,
698+ 'observed_bytes ' => 0 ,
699+ 'url ' => $ current_source_url ,
700+ );
593701 }
594702 $ rewritten = $ face ;
595703 foreach ( array_unique ( $ urls [2 ] ) as $ url ) {
596- $ parts = wp_parse_url ( $ url );
597- $ path = is_array ( $ parts ) ? strtolower ( (string ) ( $ parts ['path ' ] ?? '' ) ) : '' ;
704+ $ current_source_url = $ url ;
705+ $ parts = wp_parse_url ( $ url );
706+ $ path = is_array ( $ parts ) ? strtolower ( (string ) ( $ parts ['path ' ] ?? '' ) ) : '' ;
598707 if ( ! is_array ( $ parts ) || 'https ' !== strtolower ( (string ) ( $ parts ['scheme ' ] ?? '' ) ) || 'fonts.gstatic.com ' !== strtolower ( (string ) ( $ parts ['host ' ] ?? '' ) ) || ( isset ( $ parts ['port ' ] ) && 443 !== (int ) $ parts ['port ' ] ) || isset ( $ parts ['user ' ] ) || isset ( $ parts ['pass ' ] ) || ! preg_match ( '/\.woff2?$/ ' , $ path ) ) {
599708 $ diagnostics [] = self ::diagnostic ( 'untrusted_font_url ' );
600- return '' ;
709+ return array (
710+ 'state ' => 'preserved ' ,
711+ 'reason ' => 'untrusted_font_url ' ,
712+ 'observed_bytes ' => 0 ,
713+ 'url ' => $ url ,
714+ );
601715 }
602716 if ( ! isset ( $ payloads [ $ url ] ) ) {
603717 $ response = self ::request ( $ url , self ::FONT_LIMIT );
604718 $ payload = is_wp_error ( $ response ) ? '' : (string ) wp_remote_retrieve_body ( $ response );
605- if ( is_wp_error ( $ response ) || 200 !== (int ) wp_remote_retrieve_response_code ( $ response ) || '' === $ payload || strlen ( $ payload ) > self ::FONT_LIMIT ) {
606- $ diagnostics [] = self ::diagnostic ( 'font_payload_fetch_failed ' );
607- return '' ;
719+ $ observed = strlen ( $ payload );
720+ if ( is_wp_error ( $ response ) || 200 !== (int ) wp_remote_retrieve_response_code ( $ response ) || '' === $ payload || $ observed > self ::FONT_LIMIT ) {
721+ $ diagnostics [] = self ::diagnostic_with_detail (
722+ 'font_payload_fetch_failed ' ,
723+ array (
724+ 'url ' => $ url ,
725+ 'observed_bytes ' => $ observed ,
726+ 'limit_bytes ' => self ::FONT_LIMIT ,
727+ )
728+ );
729+ return array (
730+ 'state ' => 'preserved ' ,
731+ 'reason ' => 'font_payload_fetch_failed ' ,
732+ 'observed_bytes ' => $ observed ,
733+ 'url ' => $ url ,
734+ );
608735 }
609- if ( self ::TOTAL_FONT_LIMIT < $ payload_bytes + strlen ( $ payload ) ) {
610- $ diagnostics [] = self ::diagnostic ( 'font_payload_total_too_large ' );
611- return '' ;
736+ if ( self ::TOTAL_FONT_LIMIT < $ payload_bytes + $ observed ) {
737+ $ diagnostics [] = self ::diagnostic_with_detail (
738+ 'google_fonts_payloads_partial_preserved ' ,
739+ array (
740+ 'url ' => $ url ,
741+ 'observed_bytes ' => $ observed ,
742+ 'aggregate_bytes ' => $ payload_bytes ,
743+ 'limit_bytes ' => self ::TOTAL_FONT_LIMIT ,
744+ )
745+ );
746+ return array (
747+ 'state ' => 'preserved ' ,
748+ 'reason ' => 'google_fonts_payloads_partial_preserved ' ,
749+ 'observed_bytes ' => $ observed ,
750+ 'aggregate_bytes ' => $ payload_bytes ,
751+ 'url ' => $ url ,
752+ );
612753 }
613754 $ payloads [ $ url ] = $ payload ;
614- $ payload_bytes += strlen ( $ payload ) ;
755+ $ payload_bytes += $ observed ;
615756 }
616757 $ mime = str_ends_with ( $ path , '.woff2 ' ) ? 'font/woff2 ' : 'font/woff ' ;
617758 $ rewritten = str_replace ( $ url , 'data: ' . $ mime . ';base64, ' . base64_encode ( $ payloads [ $ url ] ), $ rewritten ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Encodes a fetched font asset for a CSS data URL.
@@ -621,7 +762,10 @@ private static function embed_font_sources( string $css, array $families, array
621762 if ( empty ( $ embedded ) ) {
622763 $ diagnostics [] = self ::diagnostic ( 'matching_font_faces_missing ' );
623764 }
624- return implode ( "\n" , $ embedded );
765+ return array (
766+ 'state ' => 'embedded ' ,
767+ 'css ' => implode ( "\n" , $ embedded ),
768+ );
625769 }
626770
627771 private static function request ( string $ url , int $ limit ) {
@@ -828,4 +972,14 @@ private static function diagnostic( string $reason ): array {
828972 'reason ' => $ reason ,
829973 );
830974 }
975+
976+ /** @return array{type:string,source:string,reason:string,details:array<string,mixed>} */
977+ private static function diagnostic_with_detail ( string $ reason , array $ details ): array {
978+ return array (
979+ 'type ' => 'font_materialization_failed ' ,
980+ 'source ' => 'static-site-importer/font-materializer ' ,
981+ 'reason ' => $ reason ,
982+ 'details ' => $ details ,
983+ );
984+ }
831985}
0 commit comments