oont-contents/plugins/wp-rocket/inc/Dependencies/RocketLazyload/Image.php
2025-02-08 15:10:23 +01:00

639 lines
17 KiB
PHP

<?php
declare(strict_types=1);
/**
* Handles lazyloading of images
*
* @package WP_Rocket\Dependencies\RocketLazyload
*/
namespace WP_Rocket\Dependencies\RocketLazyload;
/**
* A class to provide the methods needed to lazyload images in WP Rocket and Lazyload by WP Rocket
*/
class Image {
/**
* Finds the images to be lazyloaded and call the callback method to replace them.
*
* @param string $html Original HTML.
* @param string $buffer Content to parse.
* @param bool $use_native Use native lazyload.
* @return string
*/
public function lazyloadImages( $html, $buffer, $use_native = true ) {
if ( ! preg_match_all( '#<img(?<atts>\s.+)\s?/?>#iUs', $buffer, $images, PREG_SET_ORDER ) ) {
return $html;
}
$images = array_unique( $images, SORT_REGULAR );
foreach ( $images as $image ) {
$image = $this->canLazyload( $image );
if ( ! $image ) {
continue;
}
$image_lazyload = $this->replaceImage( $image, $use_native );
if ( ! $use_native ) {
$image_lazyload .= $this->noscript( $image[0] );
}
$html = str_replace( $image[0], $image_lazyload, $html );
unset( $image_lazyload );
}
return $html;
}
/**
* Applies lazyload on background images defined in style attributes
*
* @param string $html Original HTML.
* @param string $buffer Content to parse.
* @return string
*/
public function lazyloadBackgroundImages( $html, $buffer ) {
if ( ! preg_match_all( '#<(?<tag>div|figure|section|span|li|a)\s+(?<before>[^>]+[\'"\s])?style\s*=\s*([\'"])(?<styles>.*?)\3(?<after>[^>]*)>#is', $buffer, $elements, PREG_SET_ORDER ) ) {
return $html;
}
foreach ( $elements as $element ) {
if ( $this->isExcluded( $element['before'] . $element['after'], $this->getExcludedAttributes() ) ) {
continue;
}
if ( ! preg_match( '#background-image\s*:\s*(?<attr>\s*url\s*\((?<url>[^)]+)\))\s*;?#is', $element['styles'], $url ) ) {
continue;
}
if ( preg_match( '#data:image#is', $url['url'], $img ) ) {
continue;
}
$url['url'] = esc_url(
trim(
wp_strip_all_tags(
html_entity_decode(
$url['url'],
ENT_QUOTES | ENT_HTML5
)
),
'\'" '
)
);
if ( $this->isExcluded( $url['url'], $this->getExcludedSrc() ) ) {
continue;
}
$lazy_bg = $this->addLazyCLass( $element[0] );
$lazy_bg = str_replace( $url[0], '', $lazy_bg );
$lazy_bg = str_replace( '<' . $element['tag'], '<' . $element['tag'] . ' data-bg="' . esc_attr( $url['url'] ) . '"', $lazy_bg );
$html = str_replace( $element[0], $lazy_bg, $html );
unset( $lazy_bg );
}
return $html;
}
/**
* Add the identifier class to the element
*
* @param string $element Element to add the class to.
* @return string
*/
private function addLazyClass( $element ) {
$class = $this->getClasses( $element );
if ( empty( $class ) ) {
return preg_replace( '#<(img|div|figure|section|li|span|a)([^>]*)>#is', '<\1 class="rocket-lazyload"\2>', $element );
}
if ( empty( $class['attribute'] ) || empty( $class['classes'] ) ) {
return str_replace( $class['attribute'], 'class="rocket-lazyload"', $element );
}
$quotes = $this->getAttributeQuotes( $class['classes'] );
$classes = $this->trimOuterQuotes( $class['classes'], $quotes );
if ( empty( $classes ) ) {
return str_replace( $class['attribute'], 'class="rocket-lazyload"', $element );
}
$classes .= ' rocket-lazyload';
return str_replace(
$class['attribute'],
'class=' . $this->normalizeClasses( $classes, $quotes ),
$element
);
}
/**
* Gets the attribute value's outer quotation mark, if one exists, i.e. " or '.
*
* @param string $attribute_value The target attribute's value.
*
* @return bool|string quotation character; else false when no quotation mark.
*/
private function getAttributeQuotes( $attribute_value ) {
$attribute_value = trim( $attribute_value );
$first_char = $attribute_value[0];
if ( '"' === $first_char || "'" === $first_char ) {
return $first_char;
}
return false;
}
/**
* Gets the class attribute and values from the given element, if it exists.
*
* @param string $element Given HTML element to extract classes from.
*
* @return bool|string[] {
* @type string $attribute Class attribute and value, e.g. class="value"
* @type string $classes String of class attribute's value(s)
* }; else, false when no class attribute exists.
*/
private function getClasses( $element ) {
if ( ! preg_match( '#class\s*=\s*(?<classes>["\'].*?["\']|[^\s]+)#is', $element, $class ) ) {
return false;
}
if ( empty( $class ) ) {
return false;
}
if ( ! isset( $class['classes'] ) ) {
return false;
}
return [
'attribute' => $class[0],
'classes' => $class['classes'],
];
}
/**
* Removes outer single or double quotations.
*
* @param string $string String to strip quotes from.
* @param string $quotes The outer quotes to remove.
*
* @return string string without quotes.
*/
private function trimOuterQuotes( $string, $quotes ) {
$string = trim( $string );
if ( empty( $string ) ) {
return '';
}
if ( empty( $quotes ) ) {
return $string;
}
$string = ltrim( $string, $quotes );
$string = rtrim( $string, $quotes );
return trim( $string );
}
/**
* Normalizes the class attribute values to ensure well-formed.
*
* @param string $classes String of class attribute value(s).
* @param string|bool $quotes Optional. Quotation mark to wrap around the classes.
*
* @return string well-formed class attributes.
*/
private function normalizeClasses( $classes, $quotes = '"' ) {
$array_of_classes = $this->stringToArray( $classes );
$classes = implode( ' ', $array_of_classes );
if ( false === $quotes ) {
$quotes = '"';
}
return $quotes . $classes . $quotes;
}
/**
* Converts the given string into an array of strings.
*
* Note:
* 1. Removes empties.
* 2. Trims each string.
*
* @param string $string The target string to convert.
* @param string $delimiter Optional. Default: ' ' empty string.
*
* @return array An array of trimmed strings.
*/
private function stringToArray( $string, $delimiter = ' ' ) {
if ( empty( $string ) ) {
return [];
}
$array = explode( $delimiter, $string );
$array = array_map( 'trim', $array );
// Remove empties.
return array_filter( $array );
}
/**
* Applies lazyload on picture elements found in the HTML.
*
* @param string $html Original HTML.
* @param string $buffer Content to parse.
* @return string
*/
public function lazyloadPictures( $html, $buffer ) {
if ( ! preg_match_all( '#<picture(?:.*)?>(?<sources>.*)</picture>#iUs', $buffer, $pictures, PREG_SET_ORDER ) ) {
return $html;
}
$pictures = array_unique( $pictures, SORT_REGULAR );
$excluded = array_merge( $this->getExcludedAttributes(), $this->getExcludedSrc() );
foreach ( $pictures as $picture ) {
if ( $this->isExcluded( $picture[0], $excluded ) ) {
if ( ! preg_match( '#<img(?<atts>\s.+)\s?/?>#iUs', $picture[0], $img ) ) {
continue;
}
$img = $this->canLazyload( $img );
if ( ! $img ) {
continue;
}
$nolazy_picture = str_replace( '<img', '<img data-no-lazy=""', $picture[0] );
$html = str_replace( $picture[0], $nolazy_picture, $html );
continue;
}
if ( preg_match_all( '#<source(?<atts>\s.+)>#iUs', $picture['sources'], $sources, PREG_SET_ORDER ) ) {
$lazy_sources = 0;
$sources = array_unique( $sources, SORT_REGULAR );
$lazy_picture = $picture[0];
foreach ( $sources as $source ) {
$lazyload_srcset = preg_replace( '/([\s"\'])srcset/i', '\1data-lazy-srcset', $source[0] );
$lazy_picture = str_replace( $source[0], $lazyload_srcset, $lazy_picture );
unset( $lazyload_srcset );
$lazy_sources++;
}
if ( 0 === $lazy_sources ) {
continue;
}
$html = str_replace( $picture[0], $lazy_picture, $html );
}
if ( ! preg_match( '#<img(?<atts>\s.+)\s?/?>#iUs', $picture[0], $img ) ) {
continue;
}
$img = $this->canLazyload( $img );
if ( ! $img ) {
continue;
}
$img_lazy = $this->replaceImage( $img, false );
$img_lazy .= $this->noscript( $img[0] );
$safe_img = str_replace( '/', '\/', preg_quote( $img[0], '#' ) );
$html = preg_replace( '#<noscript[^>]*>.*' . $safe_img . '.*<\/noscript>(*SKIP)(*FAIL)|' . $safe_img . '#i', $img_lazy, $html );
unset( $img_lazy );
}
return $html;
}
/**
* Checks if the image can be lazyloaded
*
* @param Array $image Array of image data coming from Regex.
* @return bool|Array
*/
private function canLazyload( $image ) {
if ( $this->isExcluded( $image['atts'], $this->getExcludedAttributes() ) ) {
return false;
}
// Given the previous regex pattern, $image['atts'] starts with a whitespace character.
if ( ! preg_match( '@\ssrc\s*=\s*(\'|")(?<src>.*)\1@iUs', $image['atts'], $atts ) ) {
return false;
}
$image['src'] = trim( $atts['src'] );
if ( '' === $image['src'] ) {
return false;
}
if ( $this->isExcluded( $image['src'], $this->getExcludedSrc() ) ) {
return false;
}
return $image;
}
/**
* Checks if the provided string matches with the provided excluded patterns
*
* @param string $string String to check.
* @param array $excluded_values Patterns to match against.
* @return boolean
*/
public function isExcluded( $string, $excluded_values ) {
if ( ! is_array( $excluded_values ) ) {
$excluded_values = (array) $excluded_values;
}
if ( empty( $excluded_values ) ) {
return false;
}
foreach ( $excluded_values as $excluded_value ) {
if ( strpos( $string, $excluded_value ) !== false ) {
return true;
}
}
return false;
}
/**
* Returns the list of excluded attributes
*
* @return array
*/
public function getExcludedAttributes() {
/**
* Filters the attributes used to prevent lazylad from being applied
*
* @since 1.0
*
* @param array $excluded_attributes An array of excluded attributes.
*/
return apply_filters(
'rocket_lazyload_excluded_attributes',
[
'data-src=',
'data-no-lazy=',
'data-lazy-original=',
'data-lazy-src=',
'data-lazysrc=',
'data-lazyload=',
'data-bgposition=',
'data-envira-src=',
'fullurl=',
'lazy-slider-img=',
'data-srcset=',
'class="ls-l',
'class="ls-bg',
'soliloquy-image',
'loading="eager"',
'swatch-img',
'data-height-percentage',
'data-large_image',
'avia-bg-style-fixed',
'data-skip-lazy',
'skip-lazy',
'image-compare__',
]
);
}
/**
* Returns the list of excluded src
*
* @return array
*/
public function getExcludedSrc() {
/**
* Filters the src used to prevent lazylad from being applied
*
* @since 1.0
*
* @param array $excluded_src An array of excluded src.
*/
return apply_filters(
'rocket_lazyload_excluded_src',
[
'/wpcf7_captcha/',
'timthumb.php?src',
'woocommerce/assets/images/placeholder.png',
]
);
}
/**
* Replaces the original image by the lazyload one
*
* @param array $image Array of matches elements.
* @param bool $use_native Use native lazyload.
*
* @return string
*/
private function replaceImage( $image, $use_native = true ) {
if ( empty( $image ) ) {
return '';
}
$native_pattern = '@\sloading\s*=\s*(\'|")(?:lazy|auto)\1@i';
$image_lazyload = $image[0];
if ( $use_native ) {
if ( preg_match( $native_pattern, $image[0] ) ) {
return $image[0];
}
$image_lazyload = str_replace( '<img', '<img loading="lazy"', $image_lazyload );
} else {
$width = 0;
$height = 0;
if ( preg_match( '@[\s"\']width\s*=\s*(\'|")(?<width>.*)\1@iUs', $image['atts'], $atts ) ) {
$width = absint( $atts['width'] );
}
if ( preg_match( '@[\s"\']height\s*=\s*(\'|")(?<height>.*)\1@iUs', $image['atts'], $atts ) ) {
$height = absint( $atts['height'] );
}
$placeholder_atts = preg_replace( '@\ssrc\s*=\s*(\'|")(?<src>.*)\1@iUs', ' src="' . $this->getPlaceholder( $width, $height ) . '"', $image['atts'] );
$image_lazyload = str_replace( $image['atts'], $placeholder_atts . ' data-lazy-src="' . $image['src'] . '"', $image_lazyload );
if ( preg_match( $native_pattern, $image_lazyload ) ) {
$image_lazyload = preg_replace( $native_pattern, '', $image_lazyload );
}
}
/**
* Filter the LazyLoad HTML output
*
* @since 1.0
*
* @param string $html Output that will be printed
*/
$image_lazyload = apply_filters( 'rocket_lazyload_html', $image_lazyload );
return $image_lazyload;
}
/**
* Returns the HTML tag wrapped inside noscript tags
*
* @param string $element Element to wrap.
* @return string
*/
private function noscript( $element ) {
return '<noscript>' . $element . '</noscript>';
}
/**
* Applies lazyload on srcset and sizes attributes
*
* @param string $html HTML image tag.
* @return string
*/
public function lazyloadResponsiveAttributes( $html ) {
$html = preg_replace( '/[\s|"|\'](srcset)\s*=\s*("|\')([^"|\']+)\2/i', ' data-lazy-$1=$2$3$2', $html );
$html = preg_replace( '/[\s|"|\'](sizes)\s*=\s*("|\')([^"|\']+)\2/i', ' data-lazy-$1=$2$3$2', $html );
return $html;
}
/**
* Finds patterns matching smiley and call the callback method to replace them with the image
*
* @param string $text Content to search in.
* @return string
*/
public function convertSmilies( $text ) {
global $wp_smiliessearch;
if ( empty( $text ) || ! is_string( $text ) ) {
return $text;
}
if ( ! get_option( 'use_smilies' ) || empty( $wp_smiliessearch ) ) {
return $text;
}
$output = '';
// HTML loop taken from texturize function, could possible be consolidated.
$textarr = preg_split( '/(<.*>)/U', $text, -1, PREG_SPLIT_DELIM_CAPTURE ); // capture the tags as well as in between.
$stop = count( $textarr );// loop stuff.
// Ignore proessing of specific tags.
$tags_to_ignore = 'code|pre|style|script|textarea';
$ignore_block_element = '';
for ( $i = 0; $i < $stop; $i++ ) {
$content = $textarr[ $i ];
// If we're in an ignore block, wait until we find its closing tag.
if ( '' === $ignore_block_element && preg_match( '/^<(' . $tags_to_ignore . ')>/', $content, $matches ) ) {
$ignore_block_element = $matches[1];
}
// If it's not a tag and not in ignore block.
if ( '' === $ignore_block_element && strlen( $content ) > 0 && '<' !== $content[0] ) {
$content = preg_replace_callback( $wp_smiliessearch, [ $this, 'translateSmiley' ], $content );
}
// did we exit ignore block.
if ( '' !== $ignore_block_element && '</' . $ignore_block_element . '>' === $content ) {
$ignore_block_element = '';
}
$output .= $content;
}
return $output;
}
/**
* Replace matches by smiley image, lazyloaded
*
* @param array $matches Array of matches.
* @return string
*/
private function translateSmiley( $matches ) {
global $wpsmiliestrans;
if ( count( $matches ) === 0 ) {
return '';
}
$smiley = trim( reset( $matches ) );
$img = $wpsmiliestrans[ $smiley ];
$matches = [];
$ext = preg_match( '/\.([^.]+)$/', $img, $matches ) ? strtolower( $matches[1] ) : false;
$image_exts = [ 'jpg', 'jpeg', 'jpe', 'gif', 'png' ];
// Don't convert smilies that aren't images - they're probably emoji.
if ( ! in_array( $ext, $image_exts, true ) ) {
return $img;
}
/**
* Filter the Smiley image URL before it's used in the image element.
*
* @since 2.9.0
*
* @param string $smiley_url URL for the smiley image.
* @param string $img Filename for the smiley image.
* @param string $site_url Site URL, as returned by site_url().
*/
$src_url = apply_filters( 'smilies_src', includes_url( "images/smilies/$img" ), $img, site_url() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
// Don't LazyLoad if process is stopped for these reasons.
if ( is_feed() || is_preview() ) {
return sprintf( ' <img src="%s" alt="%s" class="wp-smiley" /> ', esc_url( $src_url ), esc_attr( $smiley ) );
}
return sprintf( ' <img src="%s" data-lazy-src="%s" alt="%s" class="wp-smiley" /> ', $this->getPlaceholder(), esc_url( $src_url ), esc_attr( $smiley ) );
}
/**
* Returns the placeholder for the src attribute
*
* @since 1.2
*
* @param int $width Width of the placeholder image. Default 0.
* @param int $height Height of the placeholder image. Default 0.
* @return string
*/
public function getPlaceholder( $width = 0, $height = 0 ) {
$width = 0 === $width ? 0 : absint( $width );
$height = 0 === $height ? 0 : absint( $height );
$placeholder = str_replace( ' ', '%20', "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 $width $height'%3E%3C/svg%3E" );
/**
* Filter the image lazyLoad placeholder on src attribute
*
* @since 1.1
*
* @param string $placeholder Placeholder that will be printed.
* @param int $width Placeholder width.
* @param int $height Placeholder height.
*/
return apply_filters( 'rocket_lazyload_placeholder', $placeholder, $width, $height );
}
}