| @@ -1,6 +1,9 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | |
| 3 | +use OptimoleWP\PageProfiler\Profile; | |
| 4 | +use OptimoleWP\Preload\Links; | |
| 5 | + | |
| 3 | 6 | /** |
| 4 | 7 | * The class handles the img tag replacements for lazyload. |
| 5 | 8 | * |
| 6 | 9 | * @package \Optml\Inc |
| @@ -8,17 +11,47 @@ | ||
| 8 | 11 | */ |
| 9 | 12 | final class Optml_Lazyload_Replacer extends Optml_App_Replacer { |
| 10 | 13 | use Optml_Normalizer; |
| 11 | 14 | use Optml_Validator; |
| 15 | + use Optml_Dam_Offload_Utils; | |
| 12 | 16 | |
| 13 | - const SVG_PLACEHOLDER = 'data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20#width#%20#height#%22%20width%3D%22#width#%22%20height%3D%22#height#%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3C%2Fsvg%3E'; | |
| 17 | + const IFRAME_PLACEHOLDER_CLASS = ' | |
| 18 | + iframe[data-opt-src]:not([data-opt-lazy-loaded]) { | |
| 19 | + background-color: #ffffff; | |
| 20 | + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2240%22%20height%3D%2240%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20style%3D%22-webkit-transform-origin%3A50%25%2050%25%3B-webkit-animation%3Aspin%201.5s%20linear%20infinite%3B-webkit-backface-visibility%3Ahidden%3Banimation%3Aspin%201.5s%20linear%20infinite%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cdefs%3E%3Cstyle%3E%3C%21%5BCDATA%5B%40-webkit-keyframes%20spin%7Bfrom%7B-webkit-transform%3Arotate%280deg%29%7Dto%7B-webkit-transform%3Arotate%28-359deg%29%7D%7D%40keyframes%20spin%7Bfrom%7Btransform%3Arotate%280deg%29%7Dto%7Btransform%3Arotate%28-359deg%29%7D%7D%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22outer%22%3E%3Cpath%20d%3D%22M20%200a3.994%203.994%200%20110%207.988A3.994%203.994%200%200120%200z%22%2F%3E%3Cpath%20d%3D%22M5.858%205.858a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23d2d2d2%22%2F%3E%3Cpath%20d%3D%22M20%2032.012A3.994%203.994%200%201120%2040a3.994%203.994%200%20010-7.988z%22%20fill%3D%22%23828282%22%2F%3E%3Cpath%20d%3D%22M28.494%2028.494a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23656565%22%2F%3E%3Cpath%20d%3D%22M3.994%2016.006a3.994%203.994%200%20110%207.988%203.994%203.994%200%20010-7.988z%22%20fill%3D%22%23bbb%22%2F%3E%3Cpath%20d%3D%22M5.858%2028.494a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23a4a4a4%22%2F%3E%3Cpath%20d%3D%22M36.006%2016.006a3.994%203.994%200%20110%207.988%203.994%203.994%200%20010-7.988z%22%20fill%3D%22%234a4a4a%22%2F%3E%3Cpath%20d%3D%22M28.494%205.858a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23323232%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); | |
| 21 | + background-repeat: no-repeat; | |
| 22 | + background-position: 50% 50%; | |
| 23 | + } | |
| 24 | + video[data-opt-src]:not([data-opt-lazy-loaded]) { | |
| 25 | + background-color: #ffffff; | |
| 26 | + background-image: url("data:image/svg+xml,%3Csvg%20width%3D%2240%22%20height%3D%2240%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20style%3D%22-webkit-transform-origin%3A50%25%2050%25%3B-webkit-animation%3Aspin%201.5s%20linear%20infinite%3B-webkit-backface-visibility%3Ahidden%3Banimation%3Aspin%201.5s%20linear%20infinite%22%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20stroke-linejoin%3D%22round%22%20stroke-miterlimit%3D%221.414%22%3E%3Cdefs%3E%3Cstyle%3E%3C%21%5BCDATA%5B%40-webkit-keyframes%20spin%7Bfrom%7B-webkit-transform%3Arotate%280deg%29%7Dto%7B-webkit-transform%3Arotate%28-359deg%29%7D%7D%40keyframes%20spin%7Bfrom%7Btransform%3Arotate%280deg%29%7Dto%7Btransform%3Arotate%28-359deg%29%7D%7D%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22outer%22%3E%3Cpath%20d%3D%22M20%200a3.994%203.994%200%20110%207.988A3.994%203.994%200%200120%200z%22%2F%3E%3Cpath%20d%3D%22M5.858%205.858a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23d2d2d2%22%2F%3E%3Cpath%20d%3D%22M20%2032.012A3.994%203.994%200%201120%2040a3.994%203.994%200%20010-7.988z%22%20fill%3D%22%23828282%22%2F%3E%3Cpath%20d%3D%22M28.494%2028.494a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23656565%22%2F%3E%3Cpath%20d%3D%22M3.994%2016.006a3.994%203.994%200%20110%207.988%203.994%203.994%200%20010-7.988z%22%20fill%3D%22%23bbb%22%2F%3E%3Cpath%20d%3D%22M5.858%2028.494a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23a4a4a4%22%2F%3E%3Cpath%20d%3D%22M36.006%2016.006a3.994%203.994%200%20110%207.988%203.994%203.994%200%20010-7.988z%22%20fill%3D%22%234a4a4a%22%2F%3E%3Cpath%20d%3D%22M28.494%205.858a3.994%203.994%200%20115.648%205.648%203.994%203.994%200%2001-5.648-5.648z%22%20fill%3D%22%23323232%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E"); | |
| 27 | + background-repeat: no-repeat; | |
| 28 | + background-position: 50% 50%; | |
| 29 | + }'; | |
| 30 | + const IFRAME_PLACEHOLDER_STYLE = '<style type="text/css">' . self::IFRAME_PLACEHOLDER_CLASS . '</style>'; | |
| 31 | + | |
| 32 | + const IFRAME_TEMP_COMMENT = '/** optmliframelazyloadplaceholder */'; | |
| 33 | + | |
| 34 | + const SVG_PLACEHOLDER = 'data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%20#width#%20#height#%22%20width%3D%22#width#%22%20height%3D%22#height#%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22#width#%22%20height%3D%22#height#%22%20fill%3D%22#fill#%22%2F%3E%3C%2Fsvg%3E'; | |
| 14 | 35 | /** |
| 36 | + * If frame lazyload is present on page. | |
| 37 | + * | |
| 38 | + * @var bool Whether or not at least one iframe has been lazyloaded. | |
| 39 | + */ | |
| 40 | + private static $found_iframe = false; | |
| 41 | + /** | |
| 15 | 42 | * Cached object instance. |
| 16 | 43 | * |
| 17 | - * @var Optml_Tag_Replacer | |
| 44 | + * @var Optml_Lazyload_Replacer | |
| 18 | 45 | */ |
| 19 | 46 | protected static $instance = null; |
| 20 | 47 | /** |
| 48 | + * Holds the number of images to skip from lazyload. | |
| 49 | + * | |
| 50 | + * @var integer The number image tags to skip. | |
| 51 | + */ | |
| 52 | + private static $skip_lazyload_images = null; | |
| 53 | + /** | |
| 21 | 54 | * Holds classes for listening to lazyload on background. |
| 22 | 55 | * |
| 23 | 56 | * @var array Lazyload background classes. |
| 24 | 57 | */ |
| @@ -58,9 +91,9 @@ | ||
| 58 | 91 | * Class instance method. |
| 59 | 92 | * |
| 60 | 93 | * @codeCoverageIgnore |
| 61 | 94 | * @static |
| 62 | - * @return Optml_Tag_Replacer | |
| 95 | + * @return Optml_Lazyload_Replacer | |
| 63 | 96 | * @since 1.0.0 |
| 64 | 97 | * @access public |
| 65 | 98 | */ |
| 66 | 99 | public static function instance() { |
| @@ -65,9 +98,9 @@ | ||
| 65 | 98 | */ |
| 66 | 99 | public static function instance() { |
| 67 | 100 | if ( null === self::$instance ) { |
| 68 | 101 | self::$instance = new self(); |
| 69 | - add_action( 'optml_replacer_setup', array( self::$instance, 'init' ) ); | |
| 102 | + add_action( 'optml_replacer_setup', [ self::$instance, 'init' ] ); | |
| 70 | 103 | } |
| 71 | 104 | |
| 72 | 105 | return self::$instance; |
| 73 | 106 | } |
| @@ -77,21 +110,26 @@ | ||
| 77 | 110 | * |
| 78 | 111 | * @return array Lazyload selectors. |
| 79 | 112 | */ |
| 80 | 113 | public static function get_background_lazyload_selectors() { |
| 114 | + if ( null !== self::$background_lazyload_selectors ) { | |
| 115 | + return self::$background_lazyload_selectors; | |
| 116 | + } | |
| 81 | 117 | |
| 82 | - if ( null != self::$background_lazyload_selectors && is_array( self::$background_lazyload_selectors ) ) { | |
| 118 | + if ( self::instance()->settings === null ) { | |
| 119 | + self::$background_lazyload_selectors = []; | |
| 120 | + | |
| 83 | 121 | return self::$background_lazyload_selectors; |
| 84 | 122 | } |
| 85 | 123 | if ( self::instance()->settings->get( 'bg_replacer' ) === 'disabled' ) { |
| 86 | 124 | self::$background_lazyload_selectors = []; |
| 87 | - | |
| 88 | 125 | return self::$background_lazyload_selectors; |
| 89 | 126 | } |
| 90 | 127 | $default_watchers = [ |
| 91 | - '.elementor-section[data-settings*="background_background"]', | |
| 92 | - '.elementor-section > .elementor-background-overlay', | |
| 93 | - '.wp-block-cover[style*="background-image"]', | |
| 128 | + '[class*="wp-block-cover"][style*="background-image"]', | |
| 129 | + '[style*="background-image:url("]', '[style*="background-image: url("]', | |
| 130 | + '[style*="background:url("]', '[style*="background: url("]', | |
| 131 | + '[class*="wp-block-group"][style*="background-image"]', | |
| 94 | 132 | ]; |
| 95 | 133 | |
| 96 | 134 | $saved_watchers = self::instance()->settings->get_watchers(); |
| 97 | 135 | |
| @@ -116,10 +154,9 @@ | ||
| 116 | 154 | * |
| 117 | 155 | * @return array |
| 118 | 156 | */ |
| 119 | 157 | public static function get_lazyload_bg_classes() { |
| 120 | - | |
| 121 | - if ( null != self::$lazyload_background_classes && is_array( self::$lazyload_background_classes ) ) { | |
| 158 | + if ( null !== self::$lazyload_background_classes ) { | |
| 122 | 159 | return self::$lazyload_background_classes; |
| 123 | 160 | } |
| 124 | 161 | |
| 125 | 162 | self::$lazyload_background_classes = apply_filters( 'optml_lazyload_bg_classes', [] ); |
| @@ -125,9 +162,22 @@ | ||
| 125 | 162 | self::$lazyload_background_classes = apply_filters( 'optml_lazyload_bg_classes', [] ); |
| 126 | 163 | |
| 127 | 164 | return self::$lazyload_background_classes; |
| 128 | 165 | } |
| 166 | + /** | |
| 167 | + * Returns the number of images to skip from lazyload. | |
| 168 | + * | |
| 169 | + * @return integer | |
| 170 | + */ | |
| 171 | + public static function get_skip_lazyload_limit() { | |
| 129 | 172 | |
| 173 | + if ( self::$skip_lazyload_images !== null ) { | |
| 174 | + return self::$skip_lazyload_images; | |
| 175 | + } | |
| 176 | + | |
| 177 | + self::$skip_lazyload_images = apply_filters( 'optml_lazyload_images_skip', self::instance()->settings->get( 'skip_lazyload_images' ) ); | |
| 178 | + return self::$skip_lazyload_images; | |
| 179 | + } | |
| 130 | 180 | /** |
| 131 | 181 | * Returns classes for lazyload additional watch. |
| 132 | 182 | * |
| 133 | 183 | * @return array |
| @@ -132,10 +182,9 @@ | ||
| 132 | 182 | * |
| 133 | 183 | * @return array |
| 134 | 184 | */ |
| 135 | 185 | public static function get_watcher_lz_classes() { |
| 136 | - | |
| 137 | - if ( null != self::$lazyload_watcher_classes && is_array( self::$lazyload_watcher_classes ) ) { | |
| 186 | + if ( null !== self::$lazyload_watcher_classes ) { | |
| 138 | 187 | return self::$lazyload_watcher_classes; |
| 139 | 188 | } |
| 140 | 189 | |
| 141 | 190 | self::$lazyload_watcher_classes = apply_filters( 'optml_watcher_lz_classes', [] ); |
| @@ -151,26 +200,45 @@ | ||
| 151 | 200 | |
| 152 | 201 | if ( ! $this->settings->use_lazyload() ) { |
| 153 | 202 | return; |
| 154 | 203 | } |
| 155 | - if ( ! Optml_Filters::should_do_page( self::$filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_URL ] ) ) { | |
| 204 | + $filters = $this->settings->get_filters(); | |
| 205 | + if ( ! Optml_Filters::should_do_page( $filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_URL ], $filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_URL_MATCH ] ) ) { | |
| 156 | 206 | return; |
| 157 | 207 | } |
| 158 | 208 | |
| 159 | - add_filter( | |
| 160 | - 'max_srcset_image_width', | |
| 161 | - function () { | |
| 162 | - return 1; | |
| 163 | - } | |
| 164 | - ); | |
| 165 | 209 | self::$is_lazyload_placeholder = self::$instance->settings->get( 'lazyload_placeholder' ) === 'enabled'; |
| 166 | 210 | |
| 167 | - add_filter( 'optml_tag_replace', array( $this, 'lazyload_tag_replace' ), 2, 6 ); | |
| 211 | + add_filter( 'optml_tag_replace', [ $this, 'lazyload_tag_replace' ], 2, 6 ); | |
| 168 | 212 | |
| 169 | - add_filter( 'optml_video_replace', array($this, 'lazyload_video_replace'), 2, 1 ); | |
| 213 | + add_filter( 'optml_video_replace', [ $this, 'lazyload_video_replace' ], 2, 1 ); | |
| 214 | + } | |
| 215 | + /** | |
| 216 | + * Check if there are lazyloaded iframes. | |
| 217 | + * | |
| 218 | + * @return bool Whether an iframe was lazyloaded on the page or not. | |
| 219 | + */ | |
| 220 | + public static function found_iframe() { | |
| 221 | + return self::$found_iframe; | |
| 222 | + } | |
| 170 | 223 | |
| 224 | + /** | |
| 225 | + * Check if the img tag should be lazyloaded early. | |
| 226 | + * | |
| 227 | + * @param string $image_tag The image tag. | |
| 228 | + * @return bool Whether the image tag should be lazyloaded early or not. | |
| 229 | + */ | |
| 230 | + public function should_lazyload_early( $image_tag ) { | |
| 231 | + $flags = apply_filters( 'optml_lazyload_early_flags', [] ); | |
| 232 | + | |
| 233 | + foreach ( $flags as $flag ) { | |
| 234 | + if ( strpos( $image_tag, $flag ) !== false ) { | |
| 235 | + return true; | |
| 236 | + } | |
| 237 | + } | |
| 238 | + | |
| 239 | + return false; | |
| 171 | 240 | } |
| 172 | - | |
| 173 | 241 | /** |
| 174 | 242 | * Replaces the tags with lazyload tags. |
| 175 | 243 | * |
| 176 | 244 | * @param string $new_tag The new tag. |
| @@ -184,16 +252,49 @@ | ||
| 184 | 252 | */ |
| 185 | 253 | public function lazyload_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed = false, $full_tag = '' ) { |
| 186 | 254 | |
| 187 | 255 | if ( ! $this->can_lazyload_for( $original_url, $full_tag ) ) { |
| 256 | + | |
| 188 | 257 | return Optml_Tag_Replacer::instance()->regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed ); |
| 189 | 258 | } |
| 259 | + // Remove fetchpriority high attribute when the image is lazyloaded | |
| 260 | + if ( strpos( $new_tag, 'fetchpriority' ) !== false ) { | |
| 261 | + $new_tag = str_ireplace( | |
| 262 | + [ | |
| 263 | + 'fetchpriority=high', | |
| 264 | + 'fetchpriority="high"', | |
| 265 | + "fetchpriority='high'", | |
| 266 | + 'fetchpriority=\"high\"', | |
| 267 | + "fetchpriority=\'high\'", | |
| 268 | + ], | |
| 269 | + '', | |
| 270 | + $new_tag | |
| 271 | + ); | |
| 272 | + } | |
| 273 | + | |
| 274 | + if ( self::instance()->settings->get( 'native_lazyload' ) === 'enabled' ) { | |
| 275 | + // First check if loading attribute already exists | |
| 276 | + if ( strpos( $new_tag, 'loading=' ) !== false ) { | |
| 277 | + // Replace existing loading attribute with lazy | |
| 278 | + $loading_attr = $is_slashed ? 'loading=\"lazy\"' : 'loading="lazy"'; | |
| 279 | + $new_tag = preg_replace( '/loading=(["\'])[^"\']*\1/i', $loading_attr, $new_tag ); | |
| 280 | + } else { | |
| 281 | + // No loading attribute exists, add it after <img | |
| 282 | + $loading_attr = $is_slashed ? ' loading=\"lazy\"' : ' loading="lazy"'; | |
| 283 | + $new_tag = preg_replace( '/<img/i', '<img' . $loading_attr, $new_tag, 1 ); | |
| 284 | + } | |
| 285 | + return $new_tag; | |
| 286 | + } | |
| 287 | + | |
| 288 | + // we remove the loading attribute if it exists with strpos | |
| 289 | + $new_tag = preg_replace( '/loading=(["\'])[^"\']*\1/i', '', $new_tag ); | |
| 290 | + | |
| 190 | 291 | $should_ignore_rescale = ! $this->is_valid_mimetype_from_url( $original_url, [ 'gif' => true, 'svg' => true ] ); |
| 191 | 292 | |
| 192 | 293 | if ( ! self::$is_lazyload_placeholder && ! $should_ignore_rescale ) { |
| 193 | 294 | $optml_args['quality'] = 'eco'; |
| 194 | 295 | $optml_args['resize'] = []; |
| 195 | - $low_url = apply_filters( 'optml_content_url', $original_url, $optml_args ); | |
| 296 | + $low_url = apply_filters( 'optml_content_url', $this->get_unoptimized_url( $original_url ), $optml_args ); | |
| 196 | 297 | $low_url = $is_slashed ? addcslashes( $low_url, '/' ) : $low_url; |
| 197 | 298 | } else { |
| 198 | 299 | $low_url = $this->get_svg_for( |
| 199 | 300 | isset( $optml_args['width'] ) ? $optml_args['width'] : '100%', |
| @@ -200,20 +301,28 @@ | ||
| 200 | 301 | isset( $optml_args['height'] ) ? $optml_args['height'] : '100%', |
| 201 | 302 | ( $should_ignore_rescale ? null : $original_url ) |
| 202 | 303 | ); |
| 203 | 304 | } |
| 204 | - | |
| 205 | 305 | $opt_format = ''; |
| 206 | 306 | |
| 207 | 307 | if ( $this->should_add_data_tag( $full_tag ) ) { |
| 208 | 308 | $opt_format = ' data-opt-src="%s" '; |
| 309 | + $custom_class = ''; | |
| 310 | + | |
| 209 | 311 | if ( $should_ignore_rescale ) { |
| 312 | + $custom_class .= 'optimole-lazy-only '; | |
| 313 | + } | |
| 314 | + if ( $this->should_lazyload_early( $full_tag ) ) { | |
| 315 | + $custom_class .= 'optimole-load-early'; | |
| 316 | + } | |
| 317 | + | |
| 318 | + if ( ! empty( $custom_class ) ) { | |
| 210 | 319 | if ( strpos( $new_tag, 'class=' ) === false ) { |
| 211 | - $opt_format .= ' class="optimole-lazy-only" '; | |
| 320 | + $opt_format .= ' class="' . $custom_class . '" '; | |
| 212 | 321 | } else { |
| 213 | 322 | $new_tag = str_replace( |
| 214 | 323 | ( $is_slashed ? 'class=\"' : 'class="' ), |
| 215 | - ( $is_slashed ? 'class=\"optimole-lazy-only ' : 'class="optimole-lazy-only ' ), | |
| 324 | + ( $is_slashed ? 'class=\"' . $custom_class . ' ' : 'class="' . $custom_class . ' ' ), | |
| 216 | 325 | $new_tag |
| 217 | 326 | ); |
| 218 | 327 | } |
| 219 | 328 | } |
| @@ -239,12 +348,16 @@ | ||
| 239 | 348 | ], |
| 240 | 349 | $new_tag, |
| 241 | 350 | 1 |
| 242 | 351 | ); |
| 352 | + | |
| 353 | + // We remove srcset and sizes attributes since our lazyload does not need them. | |
| 354 | + $pattern = '/\s+(?:srcset|sizes)=("[^"]*"|\'[^\']*\'|\\\\"[^\\\\"]*\\\\")/i'; | |
| 355 | + $new_tag = preg_replace( $pattern, '', $new_tag ); | |
| 356 | + | |
| 357 | + // We keep this for srcset lazyload compatibility that might break our mechanism. | |
| 243 | 358 | $new_tag = str_replace( 'srcset=', 'old-srcset=', $new_tag ); |
| 244 | - if ( strpos( $new_tag, 'loading=' ) === false && self::instance()->settings->get( 'native_lazyload' ) === 'enabled' ) { | |
| 245 | - $new_tag = preg_replace( '/<img/im', $is_slashed ? '<img loading=\"lazy\"' : '<img loading="lazy"', $new_tag ); | |
| 246 | - } | |
| 359 | + | |
| 247 | 360 | if ( ! $this->should_add_noscript( $new_tag ) ) { |
| 248 | 361 | return $new_tag; |
| 249 | 362 | } |
| 250 | 363 | |
| @@ -257,30 +370,49 @@ | ||
| 257 | 370 | * |
| 258 | 371 | * @return string |
| 259 | 372 | */ |
| 260 | 373 | public function lazyload_video_replace( $content ) { |
| 261 | - $video_tags = array(); | |
| 262 | - preg_match_all( '#(?:<noscript\s*>(\s*|.*?))?<iframe(.*?)></iframe>#is', $content, $video_tags ); | |
| 374 | + $video_tags = []; | |
| 375 | + $iframes = []; | |
| 376 | + $videos = []; | |
| 377 | + preg_match_all( '#(?:<noscript\s*>\s*)?<iframe(.*?)></iframe>(?:\s*</noscript\s*>)?#is', $content, $iframes ); | |
| 378 | + preg_match_all( '#(?:<noscript\s*>\s*)?<video(.*?)>.*?</video>(?:\s*</noscript\s*>)?#is', $content, $videos ); | |
| 379 | + $video_tags = array_merge( $iframes[0], $videos[0] ); | |
| 263 | 380 | |
| 264 | - $search = array(); | |
| 265 | - $replace = array(); | |
| 266 | - foreach ( $video_tags[0] as $video_tag ) { | |
| 381 | + $search = []; | |
| 382 | + $replace = []; | |
| 383 | + | |
| 384 | + foreach ( $video_tags as $video_tag ) { | |
| 385 | + | |
| 267 | 386 | if ( ! $this->should_lazyload_iframe( $video_tag ) ) { |
| 268 | 387 | continue; |
| 269 | 388 | } |
| 270 | - if ( preg_match( "/ data-opt-video-src=['\"]/is", $video_tag ) ) { | |
| 389 | + if ( preg_match( "/ data-opt-src=['\"]/is", $video_tag ) ) { | |
| 271 | 390 | continue; |
| 272 | 391 | } |
| 273 | - array_push( $search, $video_tag ); | |
| 274 | - $no_script = $video_tag; | |
| 392 | + $should_add_noscript = true; | |
| 393 | + $original_tag = $video_tag; | |
| 275 | 394 | // replace the src and add the data-opt-src attribute |
| 276 | - $video_tag = preg_replace( '/iframe(.*?)src=/is', 'iframe$1 src="about:blank" data-opt-src=', $video_tag ); | |
| 277 | - | |
| 278 | - if ( $this->should_add_noscript( $video_tag ) ) { | |
| 279 | - $video_tag .= '<noscript>' . $no_script . '</noscript>'; | |
| 395 | + if ( strpos( $video_tag, 'iframe' ) !== false ) { | |
| 396 | + $video_tag = preg_replace( '/iframe(.*?)src=/is', 'iframe$1 src="about:blank" data-opt-src=', $video_tag ); | |
| 397 | + } elseif ( strpos( $video_tag, 'video' ) !== false ) { | |
| 398 | + if ( strpos( $video_tag, 'source' ) !== false ) { | |
| 399 | + if ( strpos( $video_tag, 'preload' ) === false ) { | |
| 400 | + $video_tag = preg_replace( '/video(.*?)>/is', 'video$1 preload="metadata">', $video_tag, 1 ); | |
| 401 | + $should_add_noscript = false; | |
| 402 | + } else { | |
| 403 | + continue; | |
| 404 | + } | |
| 405 | + } else { | |
| 406 | + $video_tag = preg_replace( '/video(.*?)src=/is', 'video$1 data-opt-src=', $video_tag ); | |
| 407 | + } | |
| 280 | 408 | } |
| 409 | + if ( $this->should_add_noscript( $video_tag ) && $should_add_noscript ) { | |
| 410 | + $video_tag .= '<noscript>' . $original_tag . '</noscript>'; | |
| 411 | + } | |
| 412 | + array_push( $search, $original_tag ); | |
| 281 | 413 | array_push( $replace, $video_tag ); |
| 282 | - | |
| 414 | + self::$found_iframe = true; | |
| 283 | 415 | } |
| 284 | 416 | $search = array_unique( $search ); |
| 285 | 417 | $replace = array_unique( $replace ); |
| 286 | 418 | $content = str_replace( $search, $replace, $content ); |
| @@ -295,8 +427,11 @@ | ||
| 295 | 427 | * |
| 296 | 428 | * @return bool We can lazyload? |
| 297 | 429 | */ |
| 298 | 430 | public function can_lazyload_for( $url, $tag = '' ) { |
| 431 | + if ( OPTML_DEBUG ) { | |
| 432 | + do_action( 'optml_log', 'can_lazyload_for: ' . $url . ' ' . $tag ); | |
| 433 | + } | |
| 299 | 434 | foreach ( self::possible_lazyload_flags() as $banned_string ) { |
| 300 | 435 | if ( strpos( $tag, $banned_string ) !== false ) { |
| 301 | 436 | return false; |
| 302 | 437 | } |
| @@ -310,9 +445,9 @@ | ||
| 310 | 445 | basename( $url ), |
| 311 | 446 | Optml_Config::$image_extensions |
| 312 | 447 | ); |
| 313 | 448 | |
| 314 | - if ( ! isset( $type['ext'] ) || empty( $type['ext'] ) ) { | |
| 449 | + if ( empty( $type['ext'] ) ) { | |
| 315 | 450 | return false; |
| 316 | 451 | } |
| 317 | 452 | |
| 318 | 453 | if ( false === Optml_Filters::should_do_extension( self::$filters[ Optml_Settings::FILTER_TYPE_LAZYLOAD ][ Optml_Settings::FILTER_EXT ], $type['ext'] ) ) { |
| @@ -322,8 +457,36 @@ | ||
| 322 | 457 | if ( defined( 'OPTML_DISABLE_PNG_LAZYLOAD' ) && OPTML_DISABLE_PNG_LAZYLOAD ) { |
| 323 | 458 | return $type['ext'] !== 'png'; |
| 324 | 459 | } |
| 325 | 460 | |
| 461 | + $no_viewport_data_available = true; | |
| 462 | + | |
| 463 | + if ( $this->settings->is_lazyload_type_viewport() ) { | |
| 464 | + $image_id = $this->get_id_by_url( $url ); | |
| 465 | + $is_in_all_viewports = Optml_Manager::instance()->page_profiler->is_in_all_viewports( $image_id ); | |
| 466 | + $is_lcp_image = Optml_Manager::instance()->page_profiler->is_lcp_image_in_all_viewports( $image_id ); | |
| 467 | + $no_viewport_data_available = ! Optml_Manager::instance()->page_profiler->is_data_available(); | |
| 468 | + | |
| 469 | + if ( OPTML_DEBUG ) { | |
| 470 | + if ( $is_in_all_viewports ) { | |
| 471 | + do_action( 'optml_log', 'Lazyload skipped image is in all viewports ' . $url . '|' . $image_id ); | |
| 472 | + } elseif ( $is_lcp_image ) { | |
| 473 | + do_action( 'optml_log', 'Lazyload skipped image is LCP ' . $url . '|' . $image_id ); | |
| 474 | + } | |
| 475 | + } | |
| 476 | + | |
| 477 | + if ( $is_in_all_viewports || $is_lcp_image ) { | |
| 478 | + return false; | |
| 479 | + } | |
| 480 | + } | |
| 481 | + | |
| 482 | + if ( | |
| 483 | + $no_viewport_data_available && | |
| 484 | + $this->settings->is_lazyload_type_fixed() && | |
| 485 | + Optml_Tag_Replacer::$lazyload_skipped_images < self::get_skip_lazyload_limit() | |
| 486 | + ) { | |
| 487 | + return false; | |
| 488 | + } | |
| 326 | 489 | return true; |
| 327 | 490 | } |
| 328 | 491 | |
| 329 | 492 | /** |
| @@ -345,10 +508,10 @@ | ||
| 345 | 508 | |
| 346 | 509 | /** |
| 347 | 510 | * Get SVG markup with specific width/height. |
| 348 | 511 | * |
| 349 | - * @param int $width Markup Width. | |
| 350 | - * @param int $height Markup Height. | |
| 512 | + * @param int|string $width Markup Width. | |
| 513 | + * @param int|string $height Markup Height. | |
| 351 | 514 | * @param string|null $url Original URL. |
| 352 | 515 | * |
| 353 | 516 | * @return string SVG code. |
| 354 | 517 | */ |
| @@ -365,23 +528,39 @@ | ||
| 365 | 528 | $sizes = getimagesize( $filepath ); |
| 366 | 529 | wp_cache_add( $key, [ $sizes[0], $sizes[1] ], 'optml_sources', DAY_IN_SECONDS ); |
| 367 | 530 | } |
| 368 | 531 | } |
| 369 | - list( $width, $height ) = $sizes; | |
| 532 | + // Check if $sizes is an array before list() assignment to prevent fatal error on PHP 8.2+ | |
| 533 | + // when the image file is missing (e.g., offloaded to CDN or deleted). | |
| 534 | + if ( is_array( $sizes ) ) { | |
| 535 | + list( $width, $height ) = $sizes; | |
| 536 | + } | |
| 370 | 537 | } |
| 538 | + // If the width is not found the url might be an offloaded attachment so we can get the width and height from the metadata. | |
| 539 | + if ( ! is_numeric( $width ) && ! empty( $url ) ) { | |
| 540 | + $attachment_id = $this->attachment_url_to_post_id( $url ); | |
| 541 | + $meta = wp_get_attachment_metadata( $attachment_id ); | |
| 542 | + $width = $meta['width'] ?? false; | |
| 543 | + $height = $meta['height'] ?? false; | |
| 544 | + } | |
| 371 | 545 | |
| 372 | 546 | $width = ! is_numeric( $width ) ? '100%' : $width; |
| 373 | 547 | $height = ! is_numeric( $height ) ? '100%' : $height; |
| 548 | + $fill = $this->settings->get( 'placeholder_color' ); | |
| 374 | 549 | |
| 375 | - return | |
| 376 | - str_replace( | |
| 377 | - [ '#width#', '#height#' ], | |
| 378 | - [ | |
| 379 | - $width, | |
| 380 | - $height, | |
| 381 | - ], | |
| 382 | - self::SVG_PLACEHOLDER | |
| 383 | - ); | |
| 550 | + if ( empty( $fill ) ) { | |
| 551 | + $fill = 'transparent'; | |
| 552 | + } | |
| 553 | + | |
| 554 | + return str_replace( | |
| 555 | + [ '#width#', '#height#', '#fill#' ], | |
| 556 | + [ | |
| 557 | + $width, | |
| 558 | + $height, | |
| 559 | + urlencode( $fill ), | |
| 560 | + ], | |
| 561 | + self::SVG_PLACEHOLDER | |
| 562 | + ); | |
| 384 | 563 | } |
| 385 | 564 | |
| 386 | 565 | /** |
| 387 | 566 | * Check if we should add the noscript tag. |
| @@ -390,8 +569,11 @@ | ||
| 390 | 569 | * |
| 391 | 570 | * @return bool Should add? |
| 392 | 571 | */ |
| 393 | 572 | public function should_add_noscript( $tag ) { |
| 573 | + if ( $this->settings->get( 'no_script' ) === 'disabled' ) { | |
| 574 | + return false; | |
| 575 | + } | |
| 394 | 576 | foreach ( self::get_ignore_noscript_flags() as $banned_string ) { |
| 395 | 577 | if ( strpos( $tag, $banned_string ) !== false ) { |
| 396 | 578 | return false; |
| 397 | 579 | } |
| @@ -406,8 +588,9 @@ | ||
| 406 | 588 | * |
| 407 | 589 | * @return bool Should add? |
| 408 | 590 | */ |
| 409 | 591 | public function should_lazyload_iframe( $tag ) { |
| 592 | + | |
| 410 | 593 | foreach ( self::get_iframe_lazyload_flags() as $banned_string ) { |
| 411 | 594 | if ( strpos( $tag, $banned_string ) !== false ) { |
| 412 | 595 | return false; |
| 413 | 596 | } |
| @@ -421,14 +604,13 @@ | ||
| 421 | 604 | * |
| 422 | 605 | * @return array |
| 423 | 606 | */ |
| 424 | 607 | public static function get_ignore_noscript_flags() { |
| 425 | - | |
| 426 | - if ( null != self::$ignore_no_script_flags && is_array( self::$ignore_no_script_flags ) ) { | |
| 608 | + if ( null !== self::$ignore_no_script_flags ) { | |
| 427 | 609 | return self::$ignore_no_script_flags; |
| 428 | 610 | } |
| 429 | 611 | |
| 430 | - self::$ignore_no_script_flags = apply_filters( 'optml_ignore_noscript_on', [] ); | |
| 612 | + self::$ignore_no_script_flags = apply_filters( 'optml_ignore_noscript_on', [ '<noscript' ] ); | |
| 431 | 613 | |
| 432 | 614 | return self::$ignore_no_script_flags; |
| 433 | 615 | } |
| 434 | 616 | /** |
| @@ -436,14 +618,14 @@ | ||
| 436 | 618 | * |
| 437 | 619 | * @return array |
| 438 | 620 | */ |
| 439 | 621 | public static function get_iframe_lazyload_flags() { |
| 440 | - | |
| 441 | - if ( null != self::$iframe_lazyload_flags && is_array( self::$iframe_lazyload_flags ) ) { | |
| 622 | + if ( null !== self::$iframe_lazyload_flags ) { | |
| 442 | 623 | return self::$iframe_lazyload_flags; |
| 443 | 624 | } |
| 444 | 625 | |
| 445 | - self::$iframe_lazyload_flags = apply_filters( 'optml_iframe_lazyload_flags', [ 'gform_ajax_frame', '<noscript' ] ); | |
| 626 | + self::$iframe_lazyload_flags = self::possible_lazyload_flags(); | |
| 627 | + self::$iframe_lazyload_flags = array_merge( self::$iframe_lazyload_flags, apply_filters( 'optml_iframe_lazyload_flags', [ 'gform_ajax_frame', '<noscript', 'recaptcha', '-src' ] ) ); | |
| 446 | 628 | |
| 447 | 629 | return self::$iframe_lazyload_flags; |
| 448 | 630 | } |
| 449 | 631 | /** |
| @@ -473,6 +655,5 @@ | ||
| 473 | 655 | public function __wakeup() { |
| 474 | 656 | // Unserializing instances of the class is forbidden. |
| 475 | 657 | _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin’ huh?', 'optimole-wp' ), '1.0.0' ); |
| 476 | 658 | } |
| 477 | - | |
| 478 | 659 | } |