File "class-cache-webp.php"
Full Path: /home/theinspectionboy/public_html/suffolk/includes/class-cache-webp.php
File size: 22.99 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/**
* Implements WebP rewriting using page parsing for cache engine.
*
* @link https://ewww.io/swis/
* @package SWIS_Performance
*/
namespace SWIS;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Enables WebP URL replacement to create a separate cached page for WebP-supporting browsers.
*/
class Cache_WebP extends Page_Parser {
/**
* Allowed paths for WebP.
*
* @access protected
* @var array $webp_paths
*/
protected $webp_paths = array();
/**
* Allowed domains for WebP.
*
* @access protected
* @var array $webp_domains
*/
protected $webp_domains = array();
/**
* Setup the paths/domains for WebP Caching.
*/
function __construct() {
parent::__construct();
$upload_dir = wp_get_upload_dir();
$this->content_url = trailingslashit( ! empty( $upload_dir['baseurl'] ) ? $upload_dir['baseurl'] : content_url( 'uploads' ) );
$this->debug_message( "content_url: $this->content_url" );
$this->home_domain = $this->parse_url( $this->home_url, PHP_URL_HOST );
$this->debug_message( "home domain: $this->home_domain" );
// Find the WP Offload Media domain/path.
if ( class_exists( 'Amazon_S3_And_CloudFront' ) ) {
global $as3cf;
$s3_scheme = $as3cf->get_url_scheme();
$s3_bucket = $as3cf->get_setting( 'bucket' );
$s3_region = $as3cf->get_setting( 'region' );
if ( is_wp_error( $s3_region ) ) {
$s3_region = '';
}
if ( ! empty( $s3_bucket ) && ! is_wp_error( $s3_bucket ) && method_exists( $as3cf, 'get_provider' ) ) {
$s3_domain = $as3cf->get_provider()->get_url_domain( $s3_bucket, $s3_region, null, array(), true );
} elseif ( ! empty( $s3_bucket ) && ! is_wp_error( $s3_bucket ) && method_exists( $as3cf, 'get_storage_provider' ) ) {
$s3_domain = $as3cf->get_storage_provider()->get_url_domain( $s3_bucket, $s3_region );
}
if ( ! empty( $s3_domain ) && $as3cf->get_setting( 'serve-from-s3' ) ) {
$this->debug_message( "found S3 domain of $s3_domain with bucket $s3_bucket and region $s3_region" );
$this->webp_paths[] = $s3_scheme . '://' . $s3_domain . '/';
if ( $as3cf->get_setting( 'enable-delivery-domain' ) && $as3cf->get_setting( 'delivery-domain' ) ) {
$delivery_domain = $as3cf->get_setting( 'delivery-domain' );
$this->webp_paths[] = $s3_scheme . '://' . $delivery_domain . '/';
$this->debug_message( "found WOM delivery domain of $delivery_domain" );
}
$this->s3_active = $s3_domain;
if ( $as3cf->get_setting( 'enable-object-prefix' ) ) {
$this->s3_object_prefix = $as3cf->get_setting( 'object-prefix' );
$this->debug_message( $as3cf->get_setting( 'object-prefix' ) );
} else {
$this->debug_message( 'no WOM prefix' );
}
if ( $as3cf->get_setting( 'object-versioning' ) ) {
$this->s3_object_version = true;
$this->debug_message( 'object versioning enabled' );
}
}
}
if ( $this->get_option( 'cdn_domain' ) ) {
$this->webp_paths[] = $this->get_option( 'cdn_domain' );
}
foreach ( $this->webp_paths as $webp_path ) {
$webp_domain = $this->parse_url( $webp_path, PHP_URL_HOST );
if ( $webp_domain ) {
$this->webp_domains[] = $webp_domain;
}
}
$this->debug_message( 'checking any images matching these patterns for webp: ' . implode( ',', $this->webp_paths ) );
$this->debug_message( 'rewriting any images matching these domains to webp: ' . implode( ',', $this->webp_domains ) );
}
/**
* Replaces images within a srcset attribute with their .webp derivatives.
*
* @param string $srcset A valid srcset attribute from an img element.
* @return string The new srcset, possibly with WebP URLs.
*/
function srcset_replace( $srcset ) {
$sizes = explode( ', ', $srcset );
if ( $this->is_iterable( $sizes ) ) {
$this->debug_message( 'parsing srcset urls' );
foreach ( $sizes as $i => $size ) {
$size_parts = explode( ' ', $size );
$srcurl = $size_parts[0];
$this->debug_message( "looking for $srcurl from srcset" );
if ( $this->validate_image_url( $srcurl ) ) {
$sizes[ $i ] = str_replace( $srcurl, $this->generate_url( $srcurl ), $size );
$this->debug_message( "replaced $srcurl in srcset" );
}
}
}
$srcset = implode( ', ', $sizes );
return $srcset;
}
/**
* Replaces image URLs in the given HTML attribute with their .webp derivatives.
*
* @param string $image The original img element.
* @param string $attr The attribute to check for image URLs.
* @return string The modified img element.
*/
function attr_replace( $image, $attr ) {
$orig_url = $this->get_attribute( $image, $attr );
if ( $orig_url ) {
$this->debug_message( "looking for $attr: $orig_url" );
if ( $this->validate_image_url( $orig_url ) ) {
$this->set_attribute( $image, $attr, $this->generate_url( $orig_url ), true );
$this->debug_message( "replacing $orig_url in $attr" );
}
}
return $image;
}
/**
* Search for image URLs and rewrite them with their WebP replacements.
*
* @param string $buffer The full HTML page.
* @return string The altered HTML containing WebP image URLs.
*/
function filter_webp_html( $buffer ) {
$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
if ( $this->is_json( $buffer ) ) {
return $buffer;
}
$images = $this->get_images_from_html( $buffer, false, false );
if ( ! empty( $images[0] ) && $this->is_iterable( $images[0] ) ) {
foreach ( $images[0] as $index => $image ) {
// Ignore 0-size Pinterest schema images.
if ( strpos( $image, 'data-pin-description=' ) && strpos( $image, 'width="0" height="0"' ) ) {
continue;
}
if ( ! $this->validate_tag( $image ) ) {
continue;
}
$new_image = $image;
$file = $this->get_attribute( $image, 'src' );
$this->debug_message( "checking an image src: $file" );
if ( $this->validate_image_url( $file ) ) {
$this->set_attribute( $new_image, 'src', $this->generate_url( $file ), true );
}
$srcset = $this->get_attribute( $new_image, 'srcset' );
if ( $srcset ) {
$srcset_webp = $this->srcset_replace( $srcset );
if ( $srcset_webp !== $srcset ) {
$this->set_attribute( $new_image, 'srcset', $srcset_webp, true );
}
}
$srcset = $this->get_attribute( $new_image, 'data-srcset' );
if ( $srcset ) {
$srcset_webp = $this->srcset_replace( $srcset );
if ( $srcset_webp !== $srcset ) {
$this->set_attribute( $new_image, 'data-srcset', $srcset_webp, true );
}
}
$srcset = $this->get_attribute( $new_image, 'data-lazy-srcset' );
if ( $srcset ) {
$srcset_webp = $this->srcset_replace( $srcset );
if ( $srcset_webp !== $srcset ) {
$this->set_attribute( $new_image, 'data-lazy-srcset', $srcset_webp, true );
}
}
$new_image = $this->attr_replace( $new_image, 'data-orig-file' );
$new_image = $this->attr_replace( $new_image, 'data-medium-file' );
$new_image = $this->attr_replace( $new_image, 'data-large-file' );
$new_image = $this->attr_replace( $new_image, 'data-large_image' );
$new_image = $this->attr_replace( $new_image, 'data-src' );
$new_image = $this->attr_replace( $new_image, 'data-lazy-src' );
$new_image = $this->attr_replace( $new_image, 'data-lazysrc' );
$new_image = $this->attr_replace( $new_image, 'data-lazyload' );
// Done with the image element, everything must be replaced by now!
if ( $new_image !== $image ) {
$buffer = str_replace( $image, $new_image, $buffer );
}
} // End foreach().
} // End if().
// Images listed as picture/source elements.
$pictures = $this->get_picture_tags_from_html( $buffer );
if ( $this->is_iterable( $pictures ) ) {
foreach ( $pictures as $index => $picture ) {
if ( strpos( $picture, 'image/webp' ) ) {
continue;
}
if ( ! $this->validate_tag( $picture ) ) {
continue;
}
$sources = $this->get_elements_from_html( $picture, 'source' );
if ( $this->is_iterable( $sources ) ) {
foreach ( $sources as $source ) {
$this->debug_message( "parsing a picture source: $source" );
$srcset_attr_name = 'srcset';
if ( false !== strpos( $source, 'base64,R0lGOD' ) && false !== strpos( $source, 'data-srcset=' ) ) {
$srcset_attr_name = 'data-srcset';
}
$srcset = $this->get_attribute( $source, $srcset_attr_name );
if ( $srcset ) {
$srcset_webp = $this->srcset_replace( $srcset );
if ( $srcset_webp !== $srcset ) {
$source_webp = str_replace( $srcset, $srcset_webp, $source );
$this->set_attribute( $source_webp, 'type', 'image/webp', true );
$picture = str_replace( $source, $source_webp . $source, $picture );
}
}
}
if ( $picture !== $pictures[ $index ] ) {
$this->debug_message( 'found webp for picture element' );
$buffer = str_replace( $pictures[ $index ], $picture, $buffer );
}
}
}
}
// NextGEN slides listed as 'a' elements and LL 'a' background images.
$links = $this->get_elements_from_html( $buffer, 'a' );
if ( $this->is_iterable( $links ) ) {
foreach ( $links as $index => $link ) {
$this->debug_message( "parsing a link $link" );
if ( ! $this->validate_tag( $link ) ) {
continue;
}
$file = $this->get_attribute( $link, 'data-src' );
$thumb = $this->get_attribute( $link, 'data-thumbnail' );
if ( $file && $thumb ) {
$this->debug_message( "checking webp for ngg data-src/data-thumbnail: $file" );
$link = $this->attr_replace( $link, 'data-src' );
$link = $this->attr_replace( $link, 'data-thumbnail' );
}
$link = $this->background_image_replace( $link );
if ( $link !== $links[ $index ] ) {
$buffer = str_replace( $links[ $index ], $link, $buffer );
}
}
}
// Revolution Slider 'li' elements and LL li backgrounds.
$listitems = $this->get_elements_from_html( $buffer, 'li' );
if ( $this->is_iterable( $listitems ) ) {
foreach ( $listitems as $index => $listitem ) {
$this->debug_message( 'parsing a listitem' );
if ( ! $this->validate_tag( $listitem ) ) {
continue;
}
if ( $this->get_attribute( $listitem, 'data-title' ) === 'Slide' && ( $this->get_attribute( $listitem, 'data-lazyload' ) || $this->get_attribute( $listitem, 'data-thumb' ) ) ) {
$this->debug_message( 'checking webp for revslider data-thumb' );
$listitem = $this->attr_replace( $listitem, 'data-thumb' );
$param_num = 1;
while ( $param_num < 11 ) {
$this->debug_message( "checking webp for revslider data-param$param_num" );
$listitem = $this->attr_replace( $listitem, 'data-param' . $param_num );
$param_num++;
}
}
$listitem = $this->background_image_replace( $listitem );
if ( $listitem !== $listitems[ $index ] ) {
$buffer = str_replace( $listitems[ $index ], $listitem, $buffer );
}
} // End foreach().
} // End if().
// WooCommerce thumbs listed as 'div' elements and LL div backgrounds.
$divs = $this->get_elements_from_html( $buffer, 'div' );
if ( $this->is_iterable( $divs ) ) {
foreach ( $divs as $index => $div ) {
$this->debug_message( 'parsing a div' );
if ( ! $this->validate_tag( $div ) ) {
continue;
}
$thumb = $this->get_attribute( $div, 'data-thumb' );
$div_class = $this->get_attribute( $div, 'class' );
if ( $div_class && $thumb && strpos( $div_class, 'woocommerce-product-gallery__image' ) !== false ) {
$this->debug_message( "checking webp for WC data-thumb: $thumb" );
$div = $this->attr_replace( $div, 'data-thumb' );
}
$div = $this->background_image_replace( $div );
if ( $div !== $divs[ $index ] ) {
$buffer = str_replace( $divs[ $index ], $div, $buffer );
}
}
}
// Look for LL 'section' elements.
$sections = $this->get_elements_from_html( $buffer, 'section' );
if ( $this->is_iterable( $sections ) ) {
foreach ( $sections as $index => $section ) {
$this->debug_message( 'parsing a section' );
if ( ! $this->validate_tag( $section ) ) {
continue;
}
$section = $this->background_image_replace( $section );
if ( $section !== $sections[ $index ] ) {
$buffer = str_replace( $sections[ $index ], $section, $buffer );
}
}
}
// Look for LL 'span' elements.
$spans = $this->get_elements_from_html( $buffer, 'span' );
if ( $this->is_iterable( $spans ) ) {
foreach ( $spans as $index => $span ) {
$this->debug_message( 'parsing a span' );
if ( ! $this->validate_tag( $span ) ) {
continue;
}
$span = $this->background_image_replace( $span );
if ( $span !== $spans[ $index ] ) {
$buffer = str_replace( $spans[ $index ], $span, $buffer );
}
}
}
// Video elements, looking for poster attributes that are images.
$videos = $this->get_elements_from_html( $buffer, 'video' );
if ( $this->is_iterable( $videos ) ) {
foreach ( $videos as $index => $video ) {
$this->debug_message( 'parsing a video element' );
if ( ! $this->validate_tag( $video ) ) {
continue;
}
$file = $this->get_attribute( $video, 'poster' );
if ( $file ) {
$this->debug_message( "checking webp for video poster: $file" );
if ( $this->validate_image_url( $file ) ) {
$this->set_attribute( $video, 'poster', $this->generate_url( $file ), true );
$this->debug_message( "found webp for video poster: $file" );
$buffer = str_replace( $videos[ $index ], $video, $buffer );
}
}
}
}
$buffer = $this->filter_style_blocks( $buffer );
$this->debug_message( 'all done parsing page for webp' );
return apply_filters( 'swis_disk_cache_webp_converted_data', $buffer );
}
/**
* Parse an HTML element for inline CSS background images.
*
* @param string $element The HTML element to parse.
* @return string The modified element with WebP URLs.
*/
function background_image_replace( $element ) {
$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
$bg_image = $this->get_attribute( $element, 'data-bg' );
$class = $this->get_attribute( $element, 'class' );
if ( $class && $bg_image && false !== strpos( $class, 'lazyload' ) ) {
$this->debug_message( "checking for LL data-bg: $bg_image" );
$element = $this->attr_replace( $element, 'data-bg' );
}
if ( false === strpos( $element, 'background:' ) && false === strpos( $element, 'background-image:' ) ) {
return $element;
}
$this->debug_message( 'element contains background/background-image:' );
$style = $this->get_attribute( $element, 'style' );
if ( empty( $style ) ) {
return $element;
}
$this->debug_message( "checking style attr for background-image: $style" );
$bg_image_url = $this->get_background_image_url( $style );
if ( $bg_image_url ) {
$this->debug_message( 'bg-image url found' );
if ( $this->validate_image_url( $bg_image_url ) ) {
$this->debug_message( "found webp for $bg_image_url" );
$new_style = str_replace( $bg_image_url, $this->generate_url( $bg_image_url ), $style );
}
if ( ! empty( $new_style ) && $style !== $new_style ) {
$this->debug_message( 'style modified, continuing' );
$element = str_replace( $style, $new_style, $element );
}
}
return $element;
}
/**
* Parse page content looking for CSS blocks with background-image properties.
*
* @param string $content The HTML content to parse.
* @return string The filtered HTML content.
*/
function filter_style_blocks( $content ) {
$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
// Process background images on elements.
$elements = $this->get_style_tags_from_html( $content );
if ( $this->is_iterable( $elements ) ) {
foreach ( $elements as $eindex => $element ) {
$this->debug_message( 'parsing a style block, starts with: ' . str_replace( "\n", '', substr( $element, 0, 50 ) ) );
if ( false === strpos( $element, 'background:' ) && false === strpos( $element, 'background-image:' ) ) {
continue;
}
$bg_images = $this->get_background_images( $element );
if ( $this->is_iterable( $bg_images ) ) {
foreach ( $bg_images as $bindex => $bg_image ) {
$this->debug_message( "parsing a background CSS rule: $bg_image" );
$bg_image_url = $this->get_background_image_url( $bg_image );
$this->debug_message( "found potential background image url: $bg_image_url" );
if ( $this->validate_image_url( $bg_image_url ) ) {
$webp_bg_image_url = $this->generate_url( $bg_image_url );
if ( $bg_image_url !== $webp_bg_image_url ) {
$this->debug_message( "replacing $bg_image_url with $webp_bg_image_url" );
$bg_image = str_replace( $bg_image_url, $webp_bg_image_url, $bg_image );
if ( $bg_image !== $bg_images[ $bindex ] ) {
$this->debug_message( "replacing bg url with $bg_image" );
$element = str_replace( $bg_images[ $bindex ], $bg_image, $element );
}
}
}
}
}
if ( $element !== $elements[ $eindex ] ) {
$this->debug_message( 'replacing style block' );
$content = str_replace( $elements[ $eindex ], $element, $content );
}
}
}
return $content;
}
/**
* Attempts to reverse a CDN URL to a local path to test for file existence.
*
* Used for supporting pull-mode CDNs without forcing everything to WebP.
*
* @param string $url The image URL to mangle.
* @return bool True if a local file exists correlating to the CDN URL, false otherwise.
*/
function cdn_to_local( $url ) {
$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
if ( ! is_array( $this->webp_domains ) || ! count( $this->webp_domains ) ) {
return false;
}
foreach ( $this->webp_domains as $webp_domain ) {
if ( $webp_domain === $this->home_domain ) {
continue;
}
$this->debug_message( "looking for domain $webp_domain in $url" );
if (
! empty( $this->s3_active ) &&
false !== strpos( $url, $this->s3_active ) &&
(
( false !== strpos( $this->s3_active, '/' ) ) ||
( ! empty( $this->s3_object_prefix ) && false !== strpos( $url, $this->s3_object_prefix ) )
)
) {
// We will wait until the paths loop to fix this one.
continue;
}
if ( false !== strpos( $url, $webp_domain ) ) {
$local_url = str_replace( $webp_domain, $this->home_domain, $url );
$this->debug_message( "found $webp_domain, replaced with $this->home_domain to get $local_url" );
if ( $this->url_to_path_exists( $local_url ) ) {
return true;
}
}
}
foreach ( $this->webp_paths as $webp_path ) {
if ( false === strpos( $webp_path, 'http' ) ) {
continue;
}
$this->debug_message( "looking for path $webp_path in $url" );
if (
! empty( $this->s3_active ) &&
false !== strpos( $url, $this->s3_active ) &&
! empty( $this->s3_object_prefix ) &&
0 === strpos( $url, $webp_path . $this->s3_object_prefix )
) {
$local_url = str_replace( $webp_path . $this->s3_object_prefix, $this->content_url, $url );
$this->debug_message( "found $webp_path (and $this->s3_object_prefix), replaced with $this->content_url to get $local_url" );
if ( $this->url_to_path_exists( $local_url ) ) {
return true;
}
}
if ( false !== strpos( $url, $webp_path ) ) {
$local_url = str_replace( $webp_path, $this->content_url, $url );
$this->debug_message( "found $webp_path, replaced with $this->content_url to get $local_url" );
if ( $this->url_to_path_exists( $local_url ) ) {
return true;
}
}
}
return false;
}
/**
* Remove S3 object versioning from URL.
*
* @param string $url The image URL with a potential version string embedded.
* @return string The URL without a version string.
*/
function maybe_strip_object_version( $url ) {
if ( ! empty( $this->s3_object_version ) ) {
$possible_version = basename( dirname( $url ) );
if (
! empty( $possible_version ) &&
8 === strlen( $possible_version ) &&
ctype_digit( $possible_version )
) {
$url = str_replace( '/' . $possible_version . '/', '/', $url );
$this->debug_message( "removed version $possible_version from $url" );
} elseif (
! empty( $possible_version ) &&
14 === strlen( $possible_version ) &&
ctype_digit( $possible_version )
) {
$year = substr( $possible_version, 0, 4 );
$month = substr( $possible_version, 4, 2 );
$url = str_replace( '/' . $possible_version . '/', "/$year/$month/", $url );
$this->debug_message( "removed version $possible_version from $url" );
}
}
return $url;
}
/**
* Converts a URL to a file-system path and checks if the resulting path exists.
*
* @param string $url The URL to mangle.
* @param string $extension An optional extension to append during is_file().
* @return bool True if a local file exists correlating to the URL, false otherwise.
*/
function url_to_path_exists( $url, $extension = '' ) {
$url = $this->maybe_strip_object_version( $url );
return parent::url_to_path_exists( $url, '.webp' );
}
/**
* Checks if the tag is allowed to be rewritten.
*
* @param string $image The HTML tag: img, span, etc.
* @return bool False if it flags a filter or exclusion, true otherwise.
*/
function validate_tag( $image ) {
$this->debug_message( '<b>' . __METHOD__ . '()</b>' );
// Ignore 0-size Pinterest schema images.
if ( strpos( $image, 'data-pin-description=' ) && strpos( $image, 'width="0" height="0"' ) ) {
$this->debug_message( 'data-pin-description img skipped' );
return false;
}
$exclusions = apply_filters(
'swis_cache_webp_exclusions',
array(
'timthumb.php?',
'wpcf7_captcha/',
),
$image
);
foreach ( $exclusions as $exclusion ) {
if ( false !== strpos( $image, $exclusion ) ) {
$this->debug_message( "tag matched $exclusion" );
return false;
}
}
return true;
}
/**
* Checks if the path is a valid WebP image, on-disk or forced.
*
* @param string $image The image URL.
* @return bool True if the file exists or matches a forced path, false otherwise.
*/
function validate_image_url( $image ) {
$this->debug_message( "webp validation for $image" );
if (
strpos( $image, 'base64,R0lGOD' ) ||
strpos( $image, 'lazy-load/images/1x1' ) ||
strpos( $image, '/assets/images/' )
) {
$this->debug_message( 'lazy load placeholder' );
return false;
}
// If we got a relative image URL...
if ( '/' === substr( $image, 0, 1 ) && '/' !== substr( $image, 1, 1 ) ) {
$image = '//' . $this->home_domain . $image;
}
$extension = '';
$image_path = $this->parse_url( $image, PHP_URL_PATH );
if ( ! is_null( $image_path ) && $image_path ) {
$extension = strtolower( pathinfo( $image_path, PATHINFO_EXTENSION ) );
}
if ( $extension && 'svg' === $extension ) {
return false;
}
if ( $extension && 'webp' === $extension ) {
return false;
}
if ( apply_filters( 'swis_cache_skip_webp_rewrite', false, $image ) ) {
return false;
}
if ( $this->webp_paths && $this->webp_domains ) {
if ( $this->cdn_to_local( $image ) ) {
return true;
}
}
return $this->url_to_path_exists( $image );
}
/**
* Generate a WebP url.
*
* Adds .webp to the end, or adds a webp parameter for ExactDN urls.
*
* @param string $url The image url.
* @return string The WebP version of the image url.
*/
function generate_url( $url ) {
$path_parts = explode( '?', $url );
return $path_parts[0] . '.webp' . ( ! empty( $path_parts[1] ) && 'is-pending-load=1' !== $path_parts[1] ? '?' . $path_parts[1] : '' );
}
}