[width, height]. * * @var array */ private static $ratios = array(); /** * Open the footer buffer. Hooked on both get_footer, so a classic * theme's footer template is covered, and wp_footer, for block themes * that never fire get_footer. Whichever comes first opens it. */ public static function start(): void { if ( null !== self::$level ) { return; } if ( function_exists( 'amp_is_request' ) && amp_is_request() ) { self::$level = -1; return; } ob_start( array( __CLASS__, 'passthrough' ) ); self::$level = ob_get_level(); } /** * Output-buffer callback that changes nothing. It names the buffer, so * finish() can tell ours from one another plugin swapped in at the * same level. * * @param string $buffer Buffer contents. */ public static function passthrough( string $buffer ): string { return $buffer; } /** * Close the footer buffer and print it rewritten. * * If another plugin opened a buffer inside ours and left it open, closed * ours, or swapped its own in at the same level, the top buffer is not * ours. Closing it would swallow or reorder that plugin's output, so we * leave everything as it is and PHP flushes ours, untouched, at shutdown. */ public static function finish(): void { if ( null === self::$level || -1 === self::$level ) { return; } $level = self::$level; self::$level = -1; $status = ob_get_status(); if ( ob_get_level() !== $level || ( $status['name'] ?? '' ) !== __CLASS__ . '::passthrough' ) { return; } $html = ob_get_clean(); echo self::process( is_string( $html ) ? $html : '' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- already-rendered page markup, only attributes added. } /** Forget the buffer state, once per page render. */ public static function reset_state(): void { self::$level = null; self::$ratios = array(); } /** * Add srcset/sizes and loading="lazy" to the footer's images. * * Script, style, template and similar blocks are left alone: an * inside a JS string would break the script if we wrote double quotes * into it. * * @param string $html Footer markup. */ public static function process( string $html ): string { if ( false === stripos( $html, ']*>.*?#is', static function ( array $m ) use ( &$stubs ): string { $key = ''; $stubs[ $key ] = $m[0]; return $key; }, $html ); if ( ! is_string( $safe ) ) { return $html; } $pattern = '#"\'])*>#i'; if ( ! preg_match_all( $pattern, $safe, $tags ) ) { return $html; } // One query for every attachment instead of one per image, as // wp_filter_content_tags() does. $ids = array(); foreach ( $tags[0] as $tag ) { $id = self::attachment_id( $tag ); if ( $id > 0 && ! self::has_attr( $tag, 'srcset' ) ) { $ids[] = $id; } } if ( $ids && function_exists( '_prime_post_caches' ) ) { _prime_post_caches( array_unique( $ids ), false, true ); } self::$ratios = array(); $opts = Settings_Manager::get( 'lazy' ); $out = preg_replace_callback( $pattern, static function ( array $m ) use ( $opts ): string { return self::rewrite_img( $m[0], is_array( $opts ) ? $opts : array() ); }, $safe ); if ( ! is_string( $out ) ) { return $html; } $out = self::ratio_style() . $out; return $stubs ? strtr( $out, $stubs ) : $out; } /** * Only an attachment image that gets a srcset is touched. Lazy-loading * on its own buys nothing in the footer (see the class comment) and * would stop a hidden tracking pixel from ever loading (#558), so every * other image is left exactly as printed. * * @param array $opts Lazy settings. */ private static function rewrite_img( string $tag, array $opts ): string { $id = self::attachment_id( $tag ); if ( $id < 1 || false !== stripos( $tag, 'data-skip-lazy' ) || false !== stripos( $tag, 'data-no-lazy' ) || Lazy_Loader::has_high_fetchpriority( $tag ) || Lazy_Loader::tag_is_hidden( $tag, 'img' ) // Width and height attributes are left as they are, and a tag // that has either is skipped: every way of completing one // changes how some theme lays it out. || self::has_attr( $tag, 'srcset' ) || self::has_attr( $tag, 'sizes' ) || self::has_attr( $tag, 'width' ) || self::has_attr( $tag, 'height' ) ) { return $tag; } // sizes="auto" is only valid on a lazy image. if ( self::has_attr( $tag, 'loading' ) && ! preg_match( '#\sloading\s*=\s*["\']?lazy\b#i', $tag ) ) { return $tag; } foreach ( (array) ( $opts['excluded_images'] ?? array() ) as $pattern ) { $pattern = (string) $pattern; if ( '' !== $pattern && false !== stripos( $tag, $pattern ) ) { return $tag; } } $out = self::add_srcset( $tag, $id ); if ( $out === $tag || self::has_attr( $tag, 'loading' ) ) { return $out; } return (string) preg_replace( '#^ $dims ) { $sel = 'img[data-xspeed-ar="' . $ratio . '"]'; $css .= ':where(' . $sel . '){aspect-ratio:' . $dims[0] . '/' . $dims[1] . '}' . $sel . '{contain-intrinsic-size:' . $dims[0] . 'px ' . $dims[1] . 'px}'; } return ''; } private static function attachment_id( string $tag ): int { return preg_match( '#(?