File: /home/pclast/www/wp-content/plugins/wp-rocket/inc/Engine/Media/AboveTheFold/Frontend/Controller.php
<?php
declare(strict_types=1);
namespace WP_Rocket\Engine\Media\AboveTheFold\Frontend;
use WP_Rocket\Admin\Options_Data;
use WP_Rocket\Engine\Media\AboveTheFold\Database\Queries\AboveTheFold as ATFQuery;
use WP_Rocket\Engine\Media\AboveTheFold\Context\Context;
use WP_Rocket\Engine\Optimization\RegexTrait;
use WP_Rocket\Engine\Optimization\UrlTrait;
use WP_Rocket\Engine\Common\PerformanceHints\Frontend\ControllerInterface;
use WP_Rocket\Engine\Support\CommentTrait;
class Controller implements ControllerInterface {
use RegexTrait;
use UrlTrait;
use CommentTrait;
/**
* Options instance
*
* @var Options_Data
*/
private $options;
/**
* Queries instance
*
* @var ATFQuery
*/
private $query;
/**
* Context instance.
*
* @var Context
*/
private $context;
/**
* Constructor
*
* @param Options_Data $options Options instance.
* @param ATFQuery $query Queries instance.
* @param Context $context Context instance.
*/
public function __construct( Options_Data $options, ATFQuery $query, Context $context ) {
$this->options = $options;
$this->query = $query;
$this->context = $context;
}
/**
* Optimize the LCP image
*
* @param string $html HTML content.
* @param object $row Database Row.
*
* @return string
*/
public function optimize( string $html, $row ): string {
if ( ! $row->has_lcp() ) {
return $html;
}
$html = $this->preload_lcp( $html, $row );
return $this->add_meta_comment( 'oci', $html );
}
/**
* Preload the LCP image
*
* @param string $html HTML content.
* @param object $row Corresponding DB row.
*
* @return string
*/
private function preload_lcp( $html, $row ) {
if ( rocket_bypass() ) { // Bail out if rocket_bypass() returns true.
return $html;
}
if ( ! preg_match( '#</title\s*>#', $html, $matches ) ) {
return $html;
}
$title = $matches[0];
$preload = $title;
if ( ! $this->is_valid_data( $row->lcp ) ) {
return $html;
}
$lcp = json_decode( $row->lcp );
$preload .= $this->preload_tag( $lcp );
$replace = preg_replace( '#' . $title . '#', $preload, $html, 1 );
if ( null === $replace ) {
return $html;
}
$replace = $this->set_fetchpriority( $lcp, $replace );
return $replace;
}
/**
* Builds the preload tag
*
* @param object $lcp LCP object.
*
* @return string
*/
private function preload_tag( $lcp ): string {
$tags = $this->generate_lcp_link_tag_with_sources( $lcp );
return $tags['tags'];
}
/**
* Alters the preload element tag (img|img-srcset)
*
* @param object $lcp LCP object.
* @param string $html HTML content.
* @return string
*/
private function set_fetchpriority( $lcp, string $html ): string {
$allowed_types = [
'img',
'img-srcset',
'picture',
];
if ( empty( (array) $lcp ) || empty( $lcp->type ) || ! in_array( $lcp->type, $allowed_types, true ) ) {
return $html;
}
if ( empty( $lcp->src ) ) {
return $html;
}
$html = $this->replace_html_comments( $html );
$url = urldecode( preg_quote( $lcp->src, '/' ) );
$pattern = '#<img(?:[^>]*?\s+)?src=["\']' . $url . '["\'](?:\s+[^>]*?)?>#';
if ( wp_http_validate_url( $lcp->src ) && ! $this->is_external_file( $lcp->src ) ) {
$url = preg_quote(
wp_parse_url( $lcp->src, PHP_URL_PATH ),
'/'
);
$pattern = '#<img(?:[^>]*?\s+)?src\s*=\s*["\'](?:https?:)?(?:\/\/(?:[^\/]+)\/?)?\/?' . $url . '["\'](?:\s+[^>]*?)?>#i';
}
$html = preg_replace_callback(
$pattern,
function ( $matches ) {
// Check if the fetchpriority attribute already exists.
if ( preg_match( '/<img[^>]*\sfetchpriority(?:\s*=\s*["\'][^"\']*["\'])?[^>]*>/i', $matches[0] ) ) {
// If it exists, don't modify the tag.
return $matches[0];
}
// If it doesn't exist, add the fetchpriority attribute.
$replace = preg_replace( '/<img/', '<img fetchpriority="high"', $matches[0] );
if ( null === $replace ) {
return $matches[0];
}
return $replace;
},
$html,
1
);
return $this->restore_html_comments( $html );
}
/**
* Add above the fold images to lazyload exclusions
*
* @param array $exclusions Array of excluded patterns.
*
* @return array
*/
public function add_exclusions( $exclusions ): array {
if ( ! $this->context->is_allowed() ) {
return $exclusions;
}
list($atf, $lcp) = [ [], [] ];
global $wp;
$url = untrailingslashit( home_url( add_query_arg( [], $wp->request ) ) );
$row = $this->query->get_row( $url, $this->is_mobile() );
if ( ! $row ) {
return $exclusions;
}
if ( $row->lcp && 'not found' !== $row->lcp && $this->is_valid_data( $row->lcp ) ) {
$lcp = $this->generate_lcp_link_tag_with_sources( json_decode( $row->lcp ) );
$lcp = $lcp['sources'];
$lcp = $this->get_path_for_exclusion( $lcp );
}
if ( $row->viewport && 'not found' !== $row->viewport && $this->is_valid_data( $row->viewport ) ) {
$atf = $this->get_atf_sources( json_decode( $row->viewport ) );
$atf = $this->get_path_for_exclusion( $atf );
}
$exclusions = array_merge( $exclusions, $lcp, $atf );
// Remove lcp candidate from the atf array.
$exclusions = array_unique( $exclusions );
return $exclusions;
}
/**
* Check if lcp/viewport is valid
*
* @param string $data The lcp/viewport data.
*
* @return bool
*/
private function is_valid_data( string $data ): bool {
return ! empty( json_decode( $data, true ) );
}
/**
* Get only the url path to exclude.
*
* @param array $exclusions Array of exclusions.
* @return array
*/
private function get_path_for_exclusion( array $exclusions ): array {
$sanitized_exclusions = [];
foreach ( $exclusions as $exclusion ) {
if ( empty( $exclusion ) ) {
continue;
}
$path = wp_parse_url( $exclusion, PHP_URL_PATH );
$path = ! empty( $path ) ? ltrim( $path, '/' ) : '';
if ( empty( $path ) ) {
continue;
}
$sanitized_exclusions[] = $path;
}
return $sanitized_exclusions;
}
/**
* Generate preload link tags with sources for LCP.
*
* @param object $lcp LCP Object.
* @return array
*/
private function generate_lcp_link_tag_with_sources( object $lcp ): array {
$pairs = [
'tags' => '',
'sources' => [],
];
$tag = '';
$start_tag = '<link rel="preload" data-rocket-preload as="image" ';
$end_tag = ' fetchpriority="high">';
$sources = [];
switch ( $lcp->type ) {
case 'img':
$sources[] = $lcp->src;
$tag .= $start_tag . 'href="' . ( $this->is_relative( $lcp->src ) ? esc_attr( $lcp->src ) : esc_url( $lcp->src ) ) . '"' . $end_tag;
break;
case 'img-srcset':
$sources[] = $lcp->src;
$tag .= $start_tag . 'href="' . ( $this->is_relative( $lcp->src ) ? esc_attr( $lcp->src ) : esc_url( $lcp->src ) ) . '" imagesrcset="' . esc_attr( $lcp->srcset ) . '" imagesizes="' . esc_attr( $lcp->sizes ) . '"' . $end_tag;
break;
case 'bg-img-set':
foreach ( $lcp->bg_set as $set ) {
$sources[] = $set->src;
}
$tag .= $start_tag . 'imagesrcset="' . esc_attr( implode( ',', $sources ) ) . '"' . $end_tag;
break;
case 'bg-img':
foreach ( $lcp->bg_set as $set ) {
$sources[] = $set->src;
$tag .= $start_tag . 'href="' . ( $this->is_relative( $set->src ) ? esc_attr( $set->src ) : esc_url( $set->src ) ) . '"' . $end_tag;
}
break;
case 'picture':
$result = $this->generate_source_tags( $lcp, $start_tag, $end_tag );
$sources = $result['sources'];
$tag .= $result['tag'];
break;
}
$pairs['tags'] = $tag;
$pairs['sources'] = $sources;
return $pairs;
}
/**
* Get above the fold images sources.
*
* @param array $atfs Above the fold object.
* @return array
*/
private function get_atf_sources( array $atfs ): array {
$sources = [];
foreach ( $atfs as $atf ) {
switch ( $atf->type ) {
case 'img':
case 'img-srcset':
$sources[] = $atf->src;
break;
case 'bg-img-set':
case 'bg-img':
foreach ( $atf->bg_set as $set ) {
$sources[] = $set->src;
}
break;
case 'picture':
if ( ! empty( $atf->sources ) ) {
foreach ( $atf->sources as $source ) {
$sources[] = $source->srcset;
}
}
$sources[] = $atf->src;
break;
}
}
return $sources;
}
/**
* Determines if the page is mobile and separate cache for mobile files is enabled.
*
* @return bool
*/
private function is_mobile(): bool {
return $this->options->get( 'cache_mobile', 0 )
&& $this->options->get( 'do_caching_mobile_files', 0 )
&& wp_is_mobile();
}
/**
* Generates the source tags for the given LCP object.
*
* This method is used to generate the source tags for the given LCP object. It iterates over the sources of the LCP object,
* and for each source, it generates a media query and adds the source to the sources array. It also constructs a tag string
* with the source and media query. If a previous max-width is found, it is used to update the media query. The method also
* handles the case where a max-width is found in the source's media attribute.
*
* @param object $lcp The LCP object containing the sources.
* @param string $start_tag The starting tag for each source.
* @param string $end_tag The ending tag for each source.
*
* @return array An associative array containing the sources array and the tag string.
*/
private function generate_source_tags( $lcp, $start_tag, $end_tag ) {
$prev_max_width = null;
$sources = [];
$tag = '';
$prev_type = null;
// Iterate over the sources in the LCP object.
foreach ( $lcp->sources as $i => $source ) {
// If the type of the previous source is not equal to the type of the current source, break the loop.
if ( ! empty( $source->type ) && $prev_type !== $source->type && null !== $prev_type ) {
break;
}
$media = ! empty( $source->media ) ? $source->media : '';
// If a previous max-width is found, update the media query.
if ( null !== $prev_max_width && false === strpos( $media, 'min-width' ) ) {
$media = '(min-width: ' . ( $prev_max_width + 0.1 ) . 'px) and ' . $media;
}
// Add the media attribute to the media string with escaping.
$media = ! empty( $media ) ? sprintf( ' media="%s"', esc_attr( $media ) ) : '';
$sources[] = $source->srcset;
// Get the sizes attribute of the source, if it exists, with escaping.
$sizes = ! empty( $source->sizes ) ? sprintf( ' imagesizes="%s"', esc_attr( $source->sizes ) ) : '';
// Determine whether to use 'href' or 'imagesrcset' based on the srcset attribute.
$link_attribute = ( substr_count( $source->srcset, ',' ) > 0 ) ? 'imagesrcset' : 'href';
// Append the source and media query to the tag string with escaping.
$tag .= sprintf(
'%s%s="%s"%s%s%s',
$start_tag,
$link_attribute,
esc_attr( $source->srcset ),
$media,
$sizes,
$end_tag
);
// If a max-width is found in the source's media attribute, update the previous max-width.
if ( preg_match( '/\(max-width: (\d+(\.\d+)?)px\)/', $source->media, $matches ) ) {
$prev_max_width = floatval( $matches[1] );
}
$prev_type = $source->type;
}
// If a previous max-width is found, update the media query and add the LCP source to the sources array and the tag string.
if ( null !== $prev_max_width ) {
$media = sprintf( ' media="(min-width: %spx)"', ( $prev_max_width + 0.1 ) );
$sources[] = $lcp->src;
$tag .= sprintf(
'%shref="%s"%s%s',
$start_tag,
esc_attr( $lcp->src ),
$media,
$end_tag
);
}
// Return an associative array containing the sources array and the tag string.
return [
'sources' => $sources,
'tag' => $tag,
];
}
/**
* Add custom data like the comma-separated list of elements
* to be considered for the lcp/above-the-fold optimization.
*
* @param array $data Array of data passed in beacon.
*
* @return array
*/
public function add_custom_data( array $data ): array {
$elements = [
'img',
'video',
'picture',
'p',
'main',
'div',
'li',
'svg',
'section',
'header',
'span',
];
$default_elements = $elements;
/**
* Filters the array of elements
*
* @since 3.16
*
* @param array $formats Array of elements
*/
$elements = wpm_apply_filters_typed( 'array', 'rocket_atf_elements', $default_elements );
$elements = array_filter( $elements, 'is_string' );
$data['elements'] = implode( ', ', $elements );
$data['status']['atf'] = $this->context->is_allowed();
return $data;
}
}