PluginProbe
ezCache / 2.5
ezCache v2.5
2.6.5 2.6.4 2.6.2 2.6.3 2.6.1 2.6.0 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5 2.2.1 2.2.2 trunk 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.3 1.3.1 1.3.10 1.3.11 All 49 releases
ezcache / includes / EarlyHints.php

EarlyHints.php in ezCache 2.5, at includes/EarlyHints.php

139 lines 4.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * 103 Early Hints
4 *
5 * Sends HTTP 103 Early Hints headers for CSS and JS assets so the browser
6 * can start fetching them before the main response body arrives.
7 *
8 * @package Upress\EzCache
9 */
10 namespace Upress\EzCache;
11
12 class EarlyHints {
13
14 private static $instance;
15 private $settings;
16
17 /** @var array Collected Link header strings */
18 private $hints = [];
19
20 private function __construct() {
21 $this->settings = Settings::get_settings();
22 }
23
24 public static function instance() {
25 if ( ! self::$instance ) {
26 self::$instance = new self();
27 }
28 return self::$instance;
29 }
30
31 /**
32 * Register WordPress hooks.
33 */
34 public function register() {
35 if ( empty( $this->settings->enable_early_hints ) ) {
36 return;
37 }
38
39 // Collect enqueued styles and scripts early
40 add_action( 'wp_enqueue_scripts', [ $this, 'collect_assets' ], PHP_INT_MAX );
41
42 // Send Early Hints headers before the page is rendered.
43 // The 'send_headers' hook fires before output — ideal for 103.
44 add_action( 'send_headers', [ $this, 'send_early_hints' ] );
45 }
46
47 /**
48 * Walk through WP's registered styles & scripts and build Link headers.
49 */
50 public function collect_assets() {
51 global $wp_styles, $wp_scripts;
52
53 if ( $wp_styles instanceof \WP_Styles ) {
54 foreach ( $wp_styles->queue as $handle ) {
55 $dep = $wp_styles->registered[ $handle ] ?? null;
56 if ( $dep && $dep->src ) {
57 $src = $this->normalize_src( $dep->src );
58 if ( $src ) {
59 $this->hints[] = sprintf( '<%s>; rel=preload; as=style', esc_url_raw( $src ) );
60 }
61 }
62 }
63 }
64
65 if ( $wp_scripts instanceof \WP_Scripts ) {
66 foreach ( $wp_scripts->queue as $handle ) {
67 $dep = $wp_scripts->registered[ $handle ] ?? null;
68 if ( $dep && $dep->src ) {
69 $src = $this->normalize_src( $dep->src );
70 if ( $src ) {
71 $this->hints[] = sprintf( '<%s>; rel=preload; as=script', esc_url_raw( $src ) );
72 }
73 }
74 }
75 }
76 }
77
78 /**
79 * Emit 103 Early Hints response headers.
80 *
81 * Note: PHP can only send one status line, so we output the 103 block
82 * via header() calls before WordPress sets the 200. On servers that
83 * buffer (nginx + fastcgi_buffering off or LiteSpeed), this correctly
84 * flushes a 103 interim response first.
85 */
86 public function send_early_hints() {
87 if ( is_admin() || empty( $this->hints ) ) {
88 return;
89 }
90
91 // Only attempt on servers/PHP versions that support 103
92 if ( ! function_exists( 'header' ) ) {
93 return;
94 }
95
96 // Send 103 header line
97 if ( PHP_MAJOR_VERSION >= 8 && PHP_MINOR_VERSION >= 0 ) {
98 // PHP 8+ header() supports arbitrary status codes without output
99 @header( 'HTTP/1.1 103 Early Hints' );
100 }
101
102 // Deduplicate and send Link headers
103 $seen = [];
104 foreach ( array_unique( $this->hints ) as $hint ) {
105 $key = md5( $hint );
106 if ( isset( $seen[ $key ] ) ) { continue; }
107 $seen[ $key ] = true;
108 @header( 'Link: ' . $hint, false );
109 }
110 }
111
112 /**
113 * Normalize an asset src to an absolute URL, filtering out non-HTTP sources.
114 *
115 * @param string $src
116 * @return string|false
117 */
118 private function normalize_src( $src ) {
119 if ( ! $src ) { return false; }
120
121 // Already absolute
122 if ( strpos( $src, 'http' ) === 0 ) {
123 return $src;
124 }
125
126 // Protocol-relative
127 if ( strpos( $src, '//' ) === 0 ) {
128 return ( is_ssl() ? 'https:' : 'http:' ) . $src;
129 }
130
131 // Site-relative
132 if ( strpos( $src, '/' ) === 0 ) {
133 return get_site_url( null, $src );
134 }
135
136 return false;
137 }
138 }
139