', $lazy_lib . '', $this->content);
}
}
/**
* Parse img src for VPI preload only
* Note: Didn't reuse the _parse_img() bcoz it contains parent cls replacement and other logic which is not needed for preload
*
* @since 6.2
*/
private function _parse_img_for_preload()
{
// Load VPI setting
$is_mobile = $this->_separate_mobile();
$vpi_files = $this->cls('Metabox')->setting($is_mobile ? 'litespeed_vpi_list_mobile' : 'litespeed_vpi_list');
if ($vpi_files) {
$vpi_files = Utility::sanitize_lines($vpi_files, 'basename');
}
if (!$vpi_files) {
return;
}
$content = preg_replace(array('##sU', '##isU'), '', $this->content);
preg_match_all('#]+)/?>#isU', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$attrs = Utility::parse_attr($match[1]);
if (empty($attrs['src'])) {
continue;
}
if (strpos($attrs['src'], 'base64') !== false || substr($attrs['src'], 0, 5) === 'data:') {
Debug2::debug2('[Media] lazyload bypassed base64 img');
continue;
}
if (strpos($attrs['src'], '{') !== false) {
Debug2::debug2('[Media] image src has {} ' . $attrs['src']);
continue;
}
// If the src contains VPI filename, then preload it
if (!Utility::str_hit_array($attrs['src'], $vpi_files)) {
continue;
}
Debug2::debug2('[Media] VPI preload found and matched: ' . $attrs['src']);
$this->_vpi_preload_list[] = $attrs['src'];
}
}
/**
* Parse img src
*
* @since 1.4
* @access private
* @return array All the src & related raw html list
*/
private function _parse_img()
{
/**
* Exclude list
* @since 1.5
* @since 2.7.1 Changed to array
*/
$excludes = apply_filters('litespeed_media_lazy_img_excludes', $this->conf(Base::O_MEDIA_LAZY_EXC));
$cls_excludes = apply_filters('litespeed_media_lazy_img_cls_excludes', $this->conf(Base::O_MEDIA_LAZY_CLS_EXC));
$cls_excludes[] = 'skip-lazy'; // https://core.trac.wordpress.org/ticket/44427
$src_list = array();
$html_list = array();
$placeholder_list = array();
$content = preg_replace(
array(
'##sU',
'##isU',
'##isU', // Added to remove warning of file not found when image size detection is turned ON.
),
'',
$this->content
);
/**
* Exclude parent classes
* @since 3.0
*/
$parent_cls_exc = apply_filters('litespeed_media_lazy_img_parent_cls_excludes', $this->conf(Base::O_MEDIA_LAZY_PARENT_CLS_EXC));
if ($parent_cls_exc) {
Debug2::debug2('[Media] Lazyload Class excludes', $parent_cls_exc);
foreach ($parent_cls_exc as $v) {
$content = preg_replace('#<(\w+) [^>]*class=(\'|")[^\'"]*' . preg_quote($v, '#') . '[^\'"]*\2[^>]*>.*\1>#sU', '', $content);
}
}
preg_match_all('#]+)/?>#isU', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$attrs = Utility::parse_attr($match[1]);
if (empty($attrs['src'])) {
continue;
}
/**
* Add src validation to bypass base64 img src
* @since 1.6
*/
if (strpos($attrs['src'], 'base64') !== false || substr($attrs['src'], 0, 5) === 'data:') {
Debug2::debug2('[Media] lazyload bypassed base64 img');
continue;
}
Debug2::debug2('[Media] lazyload found: ' . $attrs['src']);
if (
!empty($attrs['data-no-lazy']) ||
!empty($attrs['data-skip-lazy']) ||
!empty($attrs['data-lazyloaded']) ||
!empty($attrs['data-src']) ||
!empty($attrs['data-srcset'])
) {
Debug2::debug2('[Media] bypassed');
continue;
}
if (!empty($attrs['class']) && ($hit = Utility::str_hit_array($attrs['class'], $cls_excludes))) {
Debug2::debug2('[Media] lazyload image cls excludes [hit] ' . $hit);
continue;
}
/**
* Exclude from lazyload by setting
* @since 1.5
*/
if ($excludes && Utility::str_hit_array($attrs['src'], $excludes)) {
Debug2::debug2('[Media] lazyload image exclude ' . $attrs['src']);
continue;
}
/**
* Excldues invalid image src from buddypress avatar crop
* @see https://wordpress.org/support/topic/lazy-load-breaking-buddypress-upload-avatar-feature
* @since 3.0
*/
if (strpos($attrs['src'], '{') !== false) {
Debug2::debug2('[Media] image src has {} ' . $attrs['src']);
continue;
}
// to avoid multiple replacement
if (in_array($match[0], $html_list)) {
continue;
}
// Add missing dimensions
if (defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_MEDIA_ADD_MISSING_SIZES)) {
if (!apply_filters('litespeed_media_add_missing_sizes', true)) {
Debug2::debug2('[Media] add_missing_sizes bypassed via litespeed_media_add_missing_sizes filter');
} elseif (empty($attrs['width']) || $attrs['width'] == 'auto' || empty($attrs['height']) || $attrs['height'] == 'auto') {
self::debug('⚠️ Missing sizes for image [src] ' . $attrs['src']);
$dimensions = $this->_detect_dimensions($attrs['src']);
if ($dimensions) {
$ori_width = $dimensions[0];
$ori_height = $dimensions[1];
// Calculate height based on width
if (!empty($attrs['width']) && $attrs['width'] != 'auto') {
$ori_height = intval(($ori_height * $attrs['width']) / $ori_width);
} elseif (!empty($attrs['height']) && $attrs['height'] != 'auto') {
$ori_width = intval(($ori_width * $attrs['height']) / $ori_height);
}
$attrs['width'] = $ori_width;
$attrs['height'] = $ori_height;
$new_html = preg_replace('#\s+(width|height)=(["\'])[^\2]*?\2#', '', $match[0]);
$new_html = preg_replace('#content = str_replace($match[0], $new_html, $this->content);
$match[0] = $new_html;
}
}
}
$placeholder = false;
if (!empty($attrs['width']) && $attrs['width'] != 'auto' && !empty($attrs['height']) && $attrs['height'] != 'auto') {
$placeholder = intval($attrs['width']) . 'x' . intval($attrs['height']);
}
$src_list[] = $attrs['src'];
$html_list[] = $match[0];
$placeholder_list[] = $placeholder;
}
return array($src_list, $html_list, $placeholder_list);
}
/**
* Detect the original sizes
*
* @since 4.0
*/
private function _detect_dimensions($src)
{
if ($pathinfo = Utility::is_internal_file($src)) {
$src = $pathinfo[0];
} elseif (apply_filters('litespeed_media_ignore_remote_missing_sizes', false)) {
return false;
}
if (substr($src, 0, 2) == '//') {
$src = 'https:' . $src;
}
try {
$sizes = getimagesize($src);
} catch (\Exception $e) {
return false;
}
if (!empty($sizes[0]) && !empty($sizes[1])) {
return $sizes;
}
return false;
}
/**
* Parse iframe src
*
* @since 1.4
* @access private
* @return array All the src & related raw html list
*/
private function _parse_iframe()
{
$cls_excludes = apply_filters('litespeed_media_iframe_lazy_cls_excludes', $this->conf(Base::O_MEDIA_IFRAME_LAZY_CLS_EXC));
$cls_excludes[] = 'skip-lazy'; // https://core.trac.wordpress.org/ticket/44427
$html_list = array();
$content = preg_replace('##sU', '', $this->content);
/**
* Exclude parent classes
* @since 3.0
*/
$parent_cls_exc = apply_filters('litespeed_media_iframe_lazy_parent_cls_excludes', $this->conf(Base::O_MEDIA_IFRAME_LAZY_PARENT_CLS_EXC));
if ($parent_cls_exc) {
Debug2::debug2('[Media] Iframe Lazyload Class excludes', $parent_cls_exc);
foreach ($parent_cls_exc as $v) {
$content = preg_replace('#<(\w+) [^>]*class=(\'|")[^\'"]*' . preg_quote($v, '#') . '[^\'"]*\2[^>]*>.*\1>#sU', '', $content);
}
}
preg_match_all('##isU', $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$attrs = Utility::parse_attr($match[1]);
if (empty($attrs['src'])) {
continue;
}
Debug2::debug2('[Media] found iframe: ' . $attrs['src']);
if (!empty($attrs['data-no-lazy']) || !empty($attrs['data-skip-lazy']) || !empty($attrs['data-lazyloaded']) || !empty($attrs['data-src'])) {
Debug2::debug2('[Media] bypassed');
continue;
}
if (!empty($attrs['class']) && ($hit = Utility::str_hit_array($attrs['class'], $cls_excludes))) {
Debug2::debug2('[Media] iframe lazyload cls excludes [hit] ' . $hit);
continue;
}
if (apply_filters('litespeed_iframe_lazyload_exc', false, $attrs['src'])) {
Debug2::debug2('[Media] bypassed by filter');
continue;
}
// to avoid multiple replacement
if (in_array($match[0], $html_list)) {
continue;
}
$html_list[] = $match[0];
}
return $html_list;
}
/**
* Replace image src to webp
*
* @since 1.6.2
* @access private
*/
private function _replace_buffer_img_webp($content)
{
/**
* Added custom element & attribute support
* @since 2.2.2
*/
$webp_ele_to_check = $this->conf(Base::O_IMG_OPTM_WEBP_ATTR);
foreach ($webp_ele_to_check as $v) {
if (!$v || strpos($v, '.') === false) {
Debug2::debug2('[Media] buffer_webp no . attribute ' . $v);
continue;
}
Debug2::debug2('[Media] buffer_webp attribute ' . $v);
$v = explode('.', $v);
$attr = preg_quote($v[1], '#');
if ($v[0]) {
$pattern = '#<' . preg_quote($v[0], '#') . '([^>]+)' . $attr . '=([\'"])(.+)\2#iU';
} else {
$pattern = '# ' . $attr . '=([\'"])(.+)\1#iU';
}
preg_match_all($pattern, $content, $matches);
foreach ($matches[$v[0] ? 3 : 2] as $k2 => $url) {
// Check if is a DATA-URI
if (strpos($url, 'data:image') !== false) {
continue;
}
if (!($url2 = $this->replace_webp($url))) {
continue;
}
if ($v[0]) {
$html_snippet = sprintf('<' . $v[0] . '%1$s' . $v[1] . '=%2$s', $matches[1][$k2], $matches[2][$k2] . $url2 . $matches[2][$k2]);
} else {
$html_snippet = sprintf(' ' . $v[1] . '=%1$s', $matches[1][$k2] . $url2 . $matches[1][$k2]);
}
$content = str_replace($matches[0][$k2], $html_snippet, $content);
}
}
// parse srcset
// todo: should apply this to cdn too
if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_IMG_OPTM_WEBP_REPLACE_SRCSET)) && $this->webp_support()) {
$content = Utility::srcset_replace($content, array($this, 'replace_webp'));
}
// Replace background-image
if ((defined('LITESPEED_GUEST_OPTM') || $this->conf(Base::O_IMG_OPTM_WEBP)) && $this->webp_support()) {
$content = $this->replace_background_webp($content);
}
return $content;
}
/**
* Replace background image
*
* @since 4.0
*/
public function replace_background_webp($content)
{
Debug2::debug2('[Media] Start replacing bakcground WebP.');
// Handle Elementors data-settings json encode background-images
$content = $this->replace_urls_in_json($content);
// preg_match_all( '#background-image:(\s*)url\((.*)\)#iU', $content, $matches );
preg_match_all('#url\(([^)]+)\)#iU', $content, $matches);
foreach ($matches[1] as $k => $url) {
// Check if is a DATA-URI
if (strpos($url, 'data:image') !== false) {
continue;
}
/**
* Support quotes in src `background-image: url('src')`
* @since 2.9.3
*/
$url = trim($url, '\'"');
// Fix Elementors Slideshow unusual background images like style="background-image: url("https://xxxx.png");"
if (strpos($url, '"') === 0 && substr($url, -6) == '"') {
$url = substr($url, 6, -6);
}
if (!($url2 = $this->replace_webp($url))) {
continue;
}
// $html_snippet = sprintf( 'background-image:%1$surl(%2$s)', $matches[ 1 ][ $k ], $url2 );
$html_snippet = str_replace($url, $url2, $matches[0][$k]);
$content = str_replace($matches[0][$k], $html_snippet, $content);
}
return $content;
}
/**
* Replace images in json data settings attributes
*
* @since 6.2
*/
public function replace_urls_in_json($content)
{
$pattern = '/data-settings="(.*?)"/i';
$parent_class = $this;
preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
// Check if the string contains HTML entities
$isEncoded = preg_match('/"|<|>|&|'/', $match[1]);
// Decode HTML entities in the JSON string
$jsonString = html_entity_decode($match[1]);
$jsonData = \json_decode($jsonString, true);
if (json_last_error() === JSON_ERROR_NONE) {
$did_webp_replace = false;
array_walk_recursive($jsonData, function (&$item, $key) use (&$did_webp_replace, $parent_class) {
if ($key == 'url') {
$item_image = $parent_class->replace_webp($item);
if ($item_image) {
$item = $item_image;
$did_webp_replace = true;
}
}
});
if ($did_webp_replace) {
// Re-encode the modified array back to a JSON string
$newJsonString = \json_encode($jsonData);
// Re-encode the JSON string to HTML entities only if it was originally encoded
if ($isEncoded) {
$newJsonString = htmlspecialchars($newJsonString, ENT_QUOTES | 0); // ENT_HTML401 is for PHPv5.4+
}
// Replace the old JSON string in the content with the new, modified JSON string
$content = str_replace($match[1], $newJsonString, $content);
}
}
}
return $content;
}
/**
* Replace internal image src to webp
*
* @since 1.6.2
* @access public
*/
public function replace_webp($url)
{
Debug2::debug2('[Media] webp replacing: ' . substr($url, 0, 200));
if (substr($url, -5) == '.webp') {
Debug2::debug2('[Media] already webp');
return false;
}
/**
* WebP API hook
* NOTE: As $url may contain query strings, WebP check will need to parse_url before appending .webp
* @since 2.9.5
* @see #751737 - API docs for WebP generation
*/
if (apply_filters('litespeed_media_check_ori', Utility::is_internal_file($url), $url)) {
// check if has webp file
if (apply_filters('litespeed_media_check_webp', Utility::is_internal_file($url, 'webp'), $url)) {
$url .= '.webp';
} else {
Debug2::debug2('[Media] -no WebP file, bypassed');
return false;
}
} else {
Debug2::debug2('[Media] -no file, bypassed');
return false;
}
Debug2::debug2('[Media] - replaced to: ' . $url);
return $url;
}
/**
* Hook to wp_get_attachment_image_src
*
* @since 1.6.2
* @access public
* @param array $img The URL of the attachment image src, the width, the height
* @return array
*/
public function webp_attach_img_src($img)
{
Debug2::debug2('[Media] changing attach src: ' . $img[0]);
if ($img && ($url = $this->replace_webp($img[0]))) {
$img[0] = $url;
}
return $img;
}
/**
* Try to replace img url
*
* @since 1.6.2
* @access public
* @param string $url
* @return string
*/
public function webp_url($url)
{
if ($url && ($url2 = $this->replace_webp($url))) {
$url = $url2;
}
return $url;
}
/**
* Hook to replace WP responsive images
*
* @since 1.6.2
* @access public
* @param array $srcs
* @return array
*/
public function webp_srcset($srcs)
{
if ($srcs) {
foreach ($srcs as $w => $data) {
if (!($url = $this->replace_webp($data['url']))) {
continue;
}
$srcs[$w]['url'] = $url;
}
}
return $srcs;
}
}