PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 3.11.1
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v3.11.1
4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 3.12.0 3.12.1 All 134 releases
optimole-wp / inc / tag_replacer.php

tag_replacer.php in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization 3.11.1, at inc/tag_replacer.php

540 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * The class handles the img tag replacements.
5 *
6 * @package \Optml\Inc
7 * @author Optimole <friends@optimole.com>
8 */
9 final class Optml_Tag_Replacer extends Optml_App_Replacer {
10 use Optml_Normalizer;
11 use Optml_Validator;
12
13 /**
14 * Cached object instance.
15 *
16 * @var Optml_Tag_Replacer
17 */
18 protected static $instance = null;
19
20 /**
21 * The number of images skipped from lazyload.
22 *
23 * @var integer
24 */
25 public static $lazyload_skipped_images = 0;
26
27 /**
28 * Class instance method.
29 *
30 * @codeCoverageIgnore
31 * @static
32 * @since 1.0.0
33 * @access public
34 * @return Optml_Tag_Replacer
35 */
36 public static function instance() {
37 if ( null === self::$instance ) {
38 self::$instance = new self();
39 add_action( 'optml_replacer_setup', [ self::$instance, 'init' ] );
40 }
41
42 return self::$instance;
43 }
44
45 /**
46 * The initialize method.
47 */
48 public function init() {
49
50 parent::init();
51 add_filter( 'optml_content_images_tags', [ $this, 'process_image_tags' ], 1, 2 );
52
53 if ( $this->settings->use_lazyload() ) {
54 return;
55 }
56
57 add_filter( 'optml_tag_replace', [ $this, 'regular_tag_replace' ], 1, 6 );
58 add_filter( 'image_downsize', [ $this, 'filter_image_downsize' ], PHP_INT_MAX, 3 );
59 add_filter( 'wp_calculate_image_srcset', [ $this, 'filter_srcset_attr' ], PHP_INT_MAX, 5 );
60 add_filter( 'wp_calculate_image_sizes', [ $this, 'filter_sizes_attr' ], 1, 2 );
61
62 }
63 /**
64 * Called by hook to replace image tags in content.
65 *
66 * @param string $content The content to process.
67 * @param array $images List of image tags.
68 *
69 * @return mixed
70 */
71
72 /**
73 * Replace in given content the given image tags with video tags is image is gif
74 *
75 * @param string $image_url Image url to process.
76 * @param string $image_tag The tag to replace.
77 * @param string $content The content to process.
78 *
79 * @return bool
80 */
81 public function img_to_video( $image_url, $image_tag, &$content ) {
82 if ( $this->settings->get( 'img_to_video' ) === 'disabled' ) {
83 return false;
84 }
85
86 if ( false === Optml_Filters::should_do_image( $image_tag, apply_filters( 'optml_gif_to_video_flags', [ 'lazyload' => true, 'placeholder' => true, 'original-src' => true ] ) ) ) {
87 return false;
88 }
89 $link_mp4 = apply_filters(
90 'optml_content_url',
91 $image_url,
92 ['width' => 'auto',
93 'height' => 'auto',
94 'format' => 'mp4',
95 ]
96 );
97
98 $link_png = apply_filters(
99 'optml_content_url',
100 $image_url,
101 ['width' => 'auto',
102 'height' => 'auto',
103 'quality' => 'eco',
104 ]
105 );
106
107 $video_tag = $image_tag;
108
109 $video_tag = str_replace(
110 [
111 'src=',
112 '<img',
113 '/>',
114 ],
115 [
116 'original-src=',
117 '<video autoplay muted loop playsinline poster="' . $link_png . '"',
118 '><source src="' . $link_mp4 . '" type="video/mp4"></video>',
119 ],
120 $video_tag
121 );
122 $content = str_replace( $image_tag, $video_tag, $content );
123 return true;
124 }
125
126 /**
127 * Method invoked by `optml_content_images_tags` filter.
128 *
129 * @param string $content The content to be processed.
130 * @param array $images A list of images.
131 *
132 * @return mixed
133 */
134 public function process_image_tags( $content, $images = [] ) {
135
136 $image_sizes = self::image_sizes();
137 $sizes2crop = self::size_to_crop();
138 foreach ( $images[0] as $index => $tag ) {
139 $width = $height = false;
140 $crop = null;
141 $image_tag = $images['img_tag'][ $index ];
142
143 $is_slashed = strpos( $images['img_url'][ $index ], '\/' ) !== false;
144
145 $src = $tmp = $is_slashed ? $this->strip_slashes( $images['img_url'][ $index ] ) : $images['img_url'][ $index ];
146
147 if ( strpos( $src, $this->upload_resource['content_path'] ) === 0 ) {
148 $src = $tmp = untrailingslashit( $this->upload_resource['content_host'] ) . $src;
149
150 $new_src = $is_slashed ? addcslashes( $src, '/' ) : $src;
151 $image_tag = str_replace(
152 [
153 '"' . $images['img_url'][ $index ],
154 "'" . $images['img_url'][ $index ],
155 ],
156 [
157 '"' . $new_src,
158 "'" . $new_src,
159 ],
160 $image_tag
161 );
162 $images['img_url'][ $index ] = $new_src;
163 }
164 if ( ( apply_filters( 'optml_ignore_image_link', false, $src ) ||
165 ( false !== strpos( $src, Optml_Config::$service_url ) && ! $this->url_has_dam_flag( $src ) ) ||
166 ! $this->can_replace_url( $src ) ||
167 ! $this->can_replace_tag( $images['img_url'][ $index ], $tag ) ) && ! Optml_Media_Offload::is_not_processed_image( $src )
168 ) {
169 continue; // @codeCoverageIgnore
170 }
171
172 $resize = apply_filters( 'optml_default_crop', [] );
173
174 list( $width, $height, $resize ) = self::parse_dimensions_from_tag(
175 $images['img_tag'][ $index ],
176 $image_sizes,
177 [
178 'width' => $width,
179 'height' => $height,
180 'resize' => $resize,
181 ]
182 );
183 if ( false === $width && false === $height ) {
184 list( $width, $height, $crop ) = $this->parse_dimensions_from_filename( $tmp );
185 }
186 if ( empty( $resize ) && isset( $sizes2crop[ $width . $height ] ) ) {
187 $resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] );
188 } elseif ( $crop === true ) {
189 $resize = $this->to_optml_crop( $crop );
190 }
191
192 $optml_args = [ 'width' => $width, 'height' => $height, 'resize' => $resize ];
193
194 $is_gif = $this->is_valid_gif( $images['img_url'][ $index ] );
195 $should_lazy_gif = $is_gif ? $this->should_lazy_gif( $images['img_url'][ $index ], $optml_args ) : null;
196
197 if ( $should_lazy_gif === true && $this->img_to_video( $images['img_url'][ $index ], $images['img_tag'][ $index ], $content ) ) {
198 continue;
199 }
200
201 $tmp = $this->strip_image_size_from_url( $tmp );
202 $new_url = apply_filters( 'optml_content_url', $tmp, $optml_args );
203
204 if ( $new_url === $tmp && ! Optml_Media_Offload::is_not_processed_image( $src ) ) {
205 continue; // @codeCoverageIgnore
206 }
207
208 $image_tag = str_replace(
209 [
210 'width="' . $width . '"',
211 'width=\"' . $width . '\"',
212 'height="' . $height . '"',
213 'height=\"' . $height . '\"',
214 ],
215 [
216 'width="' . $optml_args['width'] . '"',
217 'width=\"' . $optml_args['width'] . '\"',
218 'height="' . $optml_args['height'] . '"',
219 'height=\"' . $optml_args['height'] . '\"',
220 ],
221 $image_tag
222 );
223
224 // If the image is in header or has a class excluded from lazyload or is an excluded gif, we need to do the regular replace.
225 if ( $images['in_header'][ $index ] || $should_lazy_gif === false ) {
226 $image_tag = $this->regular_tag_replace( $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag );
227 } else {
228 $image_tag = apply_filters( 'optml_tag_replace', $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag );
229 }
230 if ( strpos( $image_tag, 'decoding=' ) === false ) {
231 $pattern = '/<img(.*?)/is';
232 $replace = '<img decoding=async $1';
233 $image_tag = preg_replace( $pattern, $replace, $image_tag );
234 }
235 $content = str_replace( $images['img_tag'][ $index ], $image_tag, $content );
236 }
237 return $content;
238 }
239 /**
240 * Check if we should lazyload a gif.
241 *
242 * @param string $url URL to check.
243 * @param array $optml_args The width/height that we find for the image.
244 * @return bool Should we lazyload the gif ?
245 */
246 public function should_lazy_gif( $url, $optml_args = [] ) {
247
248 if ( strpos( $url, '/plugins/' ) !== false ) {
249 return false;
250 }
251 if ( $this->is_valid_numeric( $optml_args['width'] ) && $this->is_valid_numeric( $optml_args['height'] ) && min( $optml_args['height'], $optml_args['width'] ) <= 20 ) {
252 return false;
253 }
254 return true;
255 }
256 /**
257 * Check replacement is allowed for this tag.
258 *
259 * @param string $url Url.
260 * @param string $tag Html tag.
261 *
262 * @return bool We can replace?
263 */
264 public function can_replace_tag( $url, $tag = '' ) {
265 foreach ( self::possible_tag_flags() as $banned_string ) {
266 if ( strpos( $tag, $banned_string ) !== false ) {
267 self::$ignored_url_map[ crc32( $url ) ] = true;
268 return false;
269 }
270 }
271 return true;
272 }
273
274 /**
275 * Extract image dimensions from img tag.
276 *
277 * @param string $tag The HTML img tag.
278 * @param array $image_sizes WordPress supported image sizes.
279 * @param array $args Default args to use.
280 *
281 * @return array
282 */
283 private function parse_dimensions_from_tag( $tag, $image_sizes, $args = [] ) {
284 if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $tag, $width_string ) ) {
285 if ( ctype_digit( $width_string[1] ) === true ) {
286 $args['width'] = $width_string[1];
287 }
288 }
289 if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $tag, $height_string ) ) {
290 if ( ctype_digit( $height_string[1] ) === true ) {
291 $args['height'] = $height_string[1];
292 }
293 }
294 if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $tag, $size ) ) {
295 $size = array_pop( $size );
296
297 if ( false === $args['width'] && false === $args['height'] && 'full' !== $size && array_key_exists( $size, $image_sizes ) ) {
298 $args['width'] = (int) $image_sizes[ $size ]['width'];
299 $args['height'] = (int) $image_sizes[ $size ]['height'];
300 }
301 if ( 'full' !== $size && array_key_exists( $size, $image_sizes ) ) {
302 $args['resize'] = $this->to_optml_crop( $image_sizes[ $size ]['crop'] );
303 }
304 } else {
305 $args['resize'] = apply_filters( 'optml_parse_resize_from_tag', [], $tag );
306 }
307
308 return [ $args['width'], $args['height'], $args['resize'] ];
309 }
310
311 /**
312 * Replaces the tags by default.
313 *
314 * @param string $new_tag The new tag.
315 * @param string $original_url The original URL.
316 * @param string $new_url The optimized URL.
317 * @param array $optml_args Options passed for URL optimization.
318 * @param bool $is_slashed Url needs to slashed.
319 * @param string $full_tag Full tag, wrapper included.
320 *
321 * @return string
322 */
323 public function regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed = false, $full_tag = '' ) {
324 $pattern = '/(?<!\/)' . preg_quote( $original_url, '/' ) . '/i';
325 $replace = $is_slashed ? addcslashes( $new_url, '/' ) : $new_url;
326 if ( $this->settings->get( 'lazyload' ) === 'enabled' && $this->settings->get( 'native_lazyload' ) === 'enabled'
327 && apply_filters( 'optml_should_load_eager', '__return_true' ) && ! $this->is_valid_gif( $original_url ) ) {
328 if ( strpos( $new_tag, 'loading=' ) === false ) {
329 $new_tag = preg_replace( '/<img/im', $is_slashed ? '<img loading=\"eager\"' : '<img loading="eager"', $new_tag );
330 } else {
331 $new_tag = $is_slashed ? str_replace( 'loading=\"lazy\"', 'loading=\"eager\"', $new_tag ) : str_replace( 'loading="lazy"', 'loading="eager"', $new_tag );
332 }
333 }
334
335 self::$lazyload_skipped_images ++;
336 return preg_replace( $pattern, $replace, $new_tag );
337 }
338
339 /**
340 * Replace image URLs in the srcset attributes and in case there is a resize in action, also replace the sizes.
341 *
342 * @param array $sources Array of image sources.
343 * @param array $size_array Array of width and height values in pixels (in that order).
344 * @param string $image_src The 'src' of the image.
345 * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
346 * @param int $attachment_id Image attachment ID.
347 *
348 * @return array
349 */
350 public function filter_srcset_attr( $sources = [], $size_array = [], $image_src = '', $image_meta = [], $attachment_id = 0 ) {
351 if ( ! is_array( $sources ) ) {
352 return $sources;
353 }
354 if ( Optml_Media_Offload::is_uploaded_image( $image_src ) ) {
355 return $sources;
356 }
357 $original_url = null;
358 $cropping = null;
359 if ( count( $size_array ) === 2 ) {
360 $sizes = self::size_to_crop();
361 $cropping = isset( $sizes[ $size_array[0] . $size_array[1] ] ) ? $this->to_optml_crop( $sizes[ $size_array[0] . $size_array[1] ] ) : null;
362 }
363
364 foreach ( $sources as $i => $source ) {
365 $url = $source['url'];
366 if ( Optml_Media_Offload::is_uploaded_image( $url ) ) {
367 continue;
368 }
369 list( $width, $height, $file_crop ) = $this->parse_dimensions_from_filename( $url );
370
371 if ( empty( $width ) ) {
372 $width = $image_meta['width'];
373 }
374
375 if ( empty( $height ) ) {
376 $height = $image_meta['height'];
377 }
378
379 if ( $original_url === null ) {
380 if ( ! empty( $attachment_id ) ) {
381 $original_url = wp_get_attachment_url( $attachment_id );
382 } else {
383 $original_url = $this->strip_image_size_from_url( $source['url'] );
384 }
385 }
386 $args = [];
387 if ( 'w' === $source['descriptor'] ) {
388 if ( $height && ( $source['value'] === $width ) ) {
389 $args['width'] = $width;
390 $args['height'] = $height;
391 } else {
392 $args['width'] = $source['value'];
393 }
394 }
395 if ( $cropping !== null ) {
396 $args['resize'] = $cropping;
397 } else {
398 $args['resize'] = $this->to_optml_crop( $file_crop );
399 }
400 $sources[ $i ]['url'] = apply_filters( 'optml_content_url', $original_url, $args );
401 }
402
403 return $sources;
404 }
405
406 /**
407 * Filters sizes attribute of the images.
408 *
409 * @param array $sizes An array of media query breakpoints.
410 * @param array $size Width and height of the image.
411 *
412 * @return mixed An array of media query breakpoints.
413 */
414 public function filter_sizes_attr( $sizes, $size ) {
415
416 if ( ! doing_filter( 'the_content' ) ) {
417 return $sizes;
418 }
419
420 $content_width = false;
421 if ( isset( $GLOBALS['content_width'] ) ) {
422 $content_width = $GLOBALS['content_width'];
423 }
424
425 if ( ! $content_width ) {
426 $content_width = 1000;
427 }
428 if ( is_array( $size ) && $size[0] < $content_width ) {
429 return $sizes;
430 }
431
432 return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
433 }
434
435 /**
436 * This filter will replace all the images retrieved via "wp_get_image" type of functions.
437 *
438 * @param array $image The filtered value.
439 * @param int $attachment_id The related attachment id.
440 * @param array|string $size This could be the name of the thumbnail size or an array of custom dimensions.
441 *
442 * @return array
443 */
444 public function filter_image_downsize( $image, $attachment_id, $size ) {
445
446 $image_url = wp_get_attachment_url( $attachment_id );
447 if ( Optml_Media_Offload::is_uploaded_image( $image_url ) ) {
448 return $image;
449 }
450 if ( $image_url === false ) {
451 return $image;
452 }
453
454 $image_meta = wp_get_attachment_metadata( $attachment_id );
455 $image_args = self::image_sizes();
456 // default size
457 $sizes = [
458 'width' => isset( $image_meta['width'] ) ? intval( $image_meta['width'] ) : false,
459 'height' => isset( $image_meta['height'] ) ? intval( $image_meta['height'] ) : false,
460 ];
461
462 switch ( $size ) {
463 case is_array( $size ):
464 $width = isset( $size[0] ) ? (int) $size[0] : false;
465 $height = isset( $size[1] ) ? (int) $size[1] : false;
466 if ( ! $width || ! $height ) {
467 break;
468 }
469 $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $width, $height );
470 if ( $image_resized ) {
471 $width = $image_resized[6];
472 $height = $image_resized[7];
473 } else {
474 $width = $image_meta['width'];
475 $height = $image_meta['height'];
476 }
477 list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $width, $height, $size );
478
479 break;
480 case 'full' !== $size && isset( $image_args[ $size ] ):
481 $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $image_args[ $size ]['width'], $image_args[ $size ]['height'], $image_args[ $size ]['crop'] );
482
483 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
484 $sizes['width'] = $image_resized[6];
485 $sizes['height'] = $image_resized[7];
486 }
487
488 list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $sizes['width'], $sizes['height'], $size, 'display' );
489
490 $sizes['resize'] = $this->to_optml_crop( $image_args[ $size ]['crop'] );
491
492 break;
493 }
494 $image_url = $this->strip_image_size_from_url( $image_url );
495
496 $new_url = apply_filters( 'optml_content_url', $image_url, $sizes );
497
498 if ( $new_url === $image_url ) {
499 return $image;
500 }
501
502 return [
503 $new_url,
504 $sizes['width'],
505 $sizes['height'],
506 $size === 'full',
507 ];
508 }
509
510 /**
511 * Throw error on object clone
512 *
513 * The whole idea of the singleton design pattern is that there is a single
514 * object therefore, we don't want the object to be cloned.
515 *
516 * @codeCoverageIgnore
517 * @access public
518 * @since 1.0.0
519 * @return void
520 */
521 public function __clone() {
522 // Cloning instances of the class is forbidden.
523 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
524 }
525
526 /**
527 * Disable unserializing of the class
528 *
529 * @codeCoverageIgnore
530 * @access public
531 * @since 1.0.0
532 * @return void
533 */
534 public function __wakeup() {
535 // Unserializing instances of the class is forbidden.
536 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
537 }
538
539 }
540