PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 3.1.0
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v3.1.0
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.1.0, at inc/tag_replacer.php

505 lines 14.7 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 if ( ! $this->is_valid_gif( $image_url ) ) {
86 return false;
87 }
88 $link_mp4 = apply_filters(
89 'optml_content_url',
90 $image_url,
91 ['width' => 'auto',
92 'height' => 'auto',
93 'format' => 'mp4',
94 ]
95 );
96
97 $link_png = apply_filters(
98 'optml_content_url',
99 $image_url,
100 ['width' => 'auto',
101 'height' => 'auto',
102 'quality' => 'eco',
103 ]
104 );
105
106 $video_tag = $image_tag;
107
108 $video_tag = str_replace(
109 [
110 'src=',
111 '<img',
112 '/>',
113 ],
114 [
115 'original-src=',
116 '<video autoplay muted loop playsinline poster="' . $link_png . '"',
117 '><source src="' . $link_mp4 . '" type="video/mp4"></video>',
118 ],
119 $video_tag
120 );
121 $content = str_replace( $image_tag, $video_tag, $content );
122 return true;
123 }
124
125 /**
126 * Method invoked by `optml_content_images_tags` filter.
127 *
128 * @param string $content The content to be processed.
129 * @param array $images A list of images.
130 *
131 * @return mixed
132 */
133 public function process_image_tags( $content, $images = [] ) {
134
135 $image_sizes = self::image_sizes();
136 $sizes2crop = self::size_to_crop();
137 foreach ( $images[0] as $index => $tag ) {
138 $width = $height = false;
139 $crop = null;
140 $image_tag = $images['img_tag'][ $index ];
141
142 $is_slashed = strpos( $images['img_url'][ $index ], '\/' ) !== false;
143
144 $src = $tmp = $is_slashed ? $this->strip_slashes( $images['img_url'][ $index ] ) : $images['img_url'][ $index ];
145
146 if ( strpos( $src, $this->upload_resource['content_path'] ) === 0 ) {
147 $src = $tmp = untrailingslashit( $this->upload_resource['content_host'] ) . $src;
148
149 $new_src = $is_slashed ? addcslashes( $src, '/' ) : $src;
150 $image_tag = str_replace(
151 [
152 '"' . $images['img_url'][ $index ],
153 "'" . $images['img_url'][ $index ],
154 ],
155 [
156 '"' . $new_src,
157 "'" . $new_src,
158 ],
159 $image_tag
160 );
161 $images['img_url'][ $index ] = $new_src;
162 }
163 if ( ( apply_filters( 'optml_ignore_image_link', false, $src ) ||
164 false !== strpos( $src, Optml_Config::$service_url ) ||
165 ! $this->can_replace_url( $src ) ||
166 ! $this->can_replace_tag( $images['img_url'][ $index ], $tag ) ) && ! Optml_Media_Offload::is_not_processed_image( $src )
167 ) {
168 continue; // @codeCoverageIgnore
169 }
170
171 if ( $this->img_to_video( $images['img_url'][ $index ], $images['img_tag'][ $index ], $content ) ) {
172 continue;
173 }
174
175 $resize = apply_filters( 'optml_default_crop', [] );
176
177 list( $width, $height, $resize ) = self::parse_dimensions_from_tag(
178 $images['img_tag'][ $index ],
179 $image_sizes,
180 [
181 'width' => $width,
182 'height' => $height,
183 'resize' => $resize,
184 ]
185 );
186 if ( false === $width && false === $height ) {
187 list( $width, $height, $crop ) = $this->parse_dimensions_from_filename( $tmp );
188 }
189 if ( empty( $resize ) && isset( $sizes2crop[ $width . $height ] ) ) {
190 $resize = $this->to_optml_crop( $sizes2crop[ $width . $height ] );
191 } elseif ( $crop === true ) {
192 $resize = $this->to_optml_crop( $crop );
193 }
194
195 $optml_args = [ 'width' => $width, 'height' => $height, 'resize' => $resize ];
196 $tmp = $this->strip_image_size_from_url( $tmp );
197 $new_url = apply_filters( 'optml_content_url', $tmp, $optml_args );
198
199 if ( $new_url === $tmp && ! Optml_Media_Offload::is_not_processed_image( $src ) ) {
200 continue; // @codeCoverageIgnore
201 }
202
203 $image_tag = str_replace(
204 [
205 'width="' . $width . '"',
206 'width=\"' . $width . '\"',
207 'height="' . $height . '"',
208 'height=\"' . $height . '\"',
209 ],
210 [
211 'width="' . $optml_args['width'] . '"',
212 'width=\"' . $optml_args['width'] . '\"',
213 'height="' . $optml_args['height'] . '"',
214 'height=\"' . $optml_args['height'] . '\"',
215 ],
216 $image_tag
217 );
218
219 // If the image is in header or has a class excluded from lazyload, we need to do the regular replace.
220 if ( $images['in_header'][ $index ] ) {
221 $image_tag = $this->regular_tag_replace( $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag );
222 } else {
223 $image_tag = apply_filters( 'optml_tag_replace', $image_tag, $images['img_url'][ $index ], $new_url, $optml_args, $is_slashed, $tag );
224 }
225 if ( strpos( $image_tag, 'decoding=' ) === false ) {
226 $pattern = '/<img(.*?)/is';
227 $replace = '<img decoding=async $1';
228 $image_tag = preg_replace( $pattern, $replace, $image_tag );
229 }
230 $content = str_replace( $images['img_tag'][ $index ], $image_tag, $content );
231 }
232 return $content;
233 }
234 /**
235 * Check replacement is allowed for this tag.
236 *
237 * @param string $url Url.
238 * @param string $tag Html tag.
239 *
240 * @return bool We can replace?
241 */
242 public function can_replace_tag( $url, $tag = '' ) {
243 foreach ( self::possible_tag_flags() as $banned_string ) {
244 if ( strpos( $tag, $banned_string ) !== false ) {
245 self::$ignored_url_map[ crc32( $url ) ] = true;
246 return false;
247 }
248 }
249 return true;
250 }
251
252 /**
253 * Extract image dimensions from img tag.
254 *
255 * @param string $tag The HTML img tag.
256 * @param array $image_sizes WordPress supported image sizes.
257 * @param array $args Default args to use.
258 *
259 * @return array
260 */
261 private function parse_dimensions_from_tag( $tag, $image_sizes, $args = [] ) {
262 if ( preg_match( '#width=["|\']?([\d%]+)["|\']?#i', $tag, $width_string ) ) {
263 $args['width'] = $width_string[1];
264 }
265 if ( preg_match( '#height=["|\']?([\d%]+)["|\']?#i', $tag, $height_string ) ) {
266 $args['height'] = $height_string[1];
267 }
268 if ( preg_match( '#class=["|\']?[^"\']*size-([^"\'\s]+)[^"\']*["|\']?#i', $tag, $size ) ) {
269 $size = array_pop( $size );
270
271 if ( false === $args['width'] && false === $args['height'] && 'full' !== $size && array_key_exists( $size, $image_sizes ) ) {
272 $args['width'] = (int) $image_sizes[ $size ]['width'];
273 $args['height'] = (int) $image_sizes[ $size ]['height'];
274 }
275 if ( 'full' !== $size && array_key_exists( $size, $image_sizes ) ) {
276 $args['resize'] = $this->to_optml_crop( $image_sizes[ $size ]['crop'] );
277 }
278 } else {
279 $args['resize'] = apply_filters( 'optml_parse_resize_from_tag', [], $tag );
280 }
281
282 return [ $args['width'], $args['height'], $args['resize'] ];
283 }
284
285 /**
286 * Replaces the tags by default.
287 *
288 * @param string $new_tag The new tag.
289 * @param string $original_url The original URL.
290 * @param string $new_url The optimized URL.
291 * @param array $optml_args Options passed for URL optimization.
292 * @param bool $is_slashed Url needs to slashed.
293 * @param string $full_tag Full tag, wrapper included.
294 *
295 * @return string
296 */
297 public function regular_tag_replace( $new_tag, $original_url, $new_url, $optml_args, $is_slashed = false, $full_tag = '' ) {
298 $pattern = '/(?<!\/)' . preg_quote( $original_url, '/' ) . '/i';
299 $replace = $is_slashed ? addcslashes( $new_url, '/' ) : $new_url;
300 self::$lazyload_skipped_images ++;
301 return preg_replace( $pattern, $replace, $new_tag );
302 }
303
304 /**
305 * Replace image URLs in the srcset attributes and in case there is a resize in action, also replace the sizes.
306 *
307 * @param array $sources Array of image sources.
308 * @param array $size_array Array of width and height values in pixels (in that order).
309 * @param string $image_src The 'src' of the image.
310 * @param array $image_meta The image meta data as returned by 'wp_get_attachment_metadata()'.
311 * @param int $attachment_id Image attachment ID.
312 *
313 * @return array
314 */
315 public function filter_srcset_attr( $sources = [], $size_array = [], $image_src = '', $image_meta = [], $attachment_id = 0 ) {
316 if ( ! is_array( $sources ) ) {
317 return $sources;
318 }
319 if ( Optml_Media_Offload::is_uploaded_image( $image_src ) ) {
320 return $sources;
321 }
322 $original_url = null;
323 $cropping = null;
324 if ( count( $size_array ) === 2 ) {
325 $sizes = self::size_to_crop();
326 $cropping = isset( $sizes[ $size_array[0] . $size_array[1] ] ) ? $this->to_optml_crop( $sizes[ $size_array[0] . $size_array[1] ] ) : null;
327 }
328
329 foreach ( $sources as $i => $source ) {
330 $url = $source['url'];
331 if ( Optml_Media_Offload::is_uploaded_image( $url ) ) {
332 continue;
333 }
334 list( $width, $height, $file_crop ) = $this->parse_dimensions_from_filename( $url );
335
336 if ( empty( $width ) ) {
337 $width = $image_meta['width'];
338 }
339
340 if ( empty( $height ) ) {
341 $height = $image_meta['height'];
342 }
343
344 if ( $original_url === null ) {
345 if ( ! empty( $attachment_id ) ) {
346 $original_url = wp_get_attachment_url( $attachment_id );
347 } else {
348 $original_url = $this->strip_image_size_from_url( $source['url'] );
349 }
350 }
351 $args = [];
352 if ( 'w' === $source['descriptor'] ) {
353 if ( $height && ( $source['value'] === $width ) ) {
354 $args['width'] = $width;
355 $args['height'] = $height;
356 } else {
357 $args['width'] = $source['value'];
358 }
359 }
360 if ( $cropping !== null ) {
361 $args['resize'] = $cropping;
362 } else {
363 $args['resize'] = $this->to_optml_crop( $file_crop );
364 }
365 $sources[ $i ]['url'] = apply_filters( 'optml_content_url', $original_url, $args );
366 }
367
368 return $sources;
369 }
370
371 /**
372 * Filters sizes attribute of the images.
373 *
374 * @param array $sizes An array of media query breakpoints.
375 * @param array $size Width and height of the image.
376 *
377 * @return mixed An array of media query breakpoints.
378 */
379 public function filter_sizes_attr( $sizes, $size ) {
380
381 if ( ! doing_filter( 'the_content' ) ) {
382 return $sizes;
383 }
384
385 $content_width = false;
386 if ( isset( $GLOBALS['content_width'] ) ) {
387 $content_width = $GLOBALS['content_width'];
388 }
389
390 if ( ! $content_width ) {
391 $content_width = 1000;
392 }
393 if ( is_array( $size ) && $size[0] < $content_width ) {
394 return $sizes;
395 }
396
397 return sprintf( '(max-width: %1$dpx) 100vw, %1$dpx', $content_width );
398 }
399
400 /**
401 * This filter will replace all the images retrieved via "wp_get_image" type of functions.
402 *
403 * @param array $image The filtered value.
404 * @param int $attachment_id The related attachment id.
405 * @param array|string $size This could be the name of the thumbnail size or an array of custom dimensions.
406 *
407 * @return array
408 */
409 public function filter_image_downsize( $image, $attachment_id, $size ) {
410
411 $image_url = wp_get_attachment_url( $attachment_id );
412 if ( Optml_Media_Offload::is_uploaded_image( $image_url ) ) {
413 return $image;
414 }
415 if ( $image_url === false ) {
416 return $image;
417 }
418
419 $image_meta = wp_get_attachment_metadata( $attachment_id );
420 $image_args = self::image_sizes();
421 // default size
422 $sizes = [
423 'width' => isset( $image_meta['width'] ) ? intval( $image_meta['width'] ) : false,
424 'height' => isset( $image_meta['height'] ) ? intval( $image_meta['height'] ) : false,
425 ];
426
427 switch ( $size ) {
428 case is_array( $size ):
429 $width = isset( $size[0] ) ? (int) $size[0] : false;
430 $height = isset( $size[1] ) ? (int) $size[1] : false;
431 if ( ! $width || ! $height ) {
432 break;
433 }
434 $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $width, $height );
435 if ( $image_resized ) {
436 $width = $image_resized[6];
437 $height = $image_resized[7];
438 } else {
439 $width = $image_meta['width'];
440 $height = $image_meta['height'];
441 }
442 list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $width, $height, $size );
443
444 break;
445 case 'full' !== $size && isset( $image_args[ $size ] ):
446 $image_resized = image_resize_dimensions( $sizes['width'], $sizes['height'], $image_args[ $size ]['width'], $image_args[ $size ]['height'], $image_args[ $size ]['crop'] );
447
448 if ( $image_resized ) { // This could be false when the requested image size is larger than the full-size image.
449 $sizes['width'] = $image_resized[6];
450 $sizes['height'] = $image_resized[7];
451 }
452
453 list( $sizes['width'], $sizes['height'] ) = image_constrain_size_for_editor( $sizes['width'], $sizes['height'], $size, 'display' );
454
455 $sizes['resize'] = $this->to_optml_crop( $image_args[ $size ]['crop'] );
456
457 break;
458 }
459 $image_url = $this->strip_image_size_from_url( $image_url );
460
461 $new_url = apply_filters( 'optml_content_url', $image_url, $sizes );
462
463 if ( $new_url === $image_url ) {
464 return $image;
465 }
466
467 return [
468 $new_url,
469 $sizes['width'],
470 $sizes['height'],
471 $size === 'full',
472 ];
473 }
474
475 /**
476 * Throw error on object clone
477 *
478 * The whole idea of the singleton design pattern is that there is a single
479 * object therefore, we don't want the object to be cloned.
480 *
481 * @codeCoverageIgnore
482 * @access public
483 * @since 1.0.0
484 * @return void
485 */
486 public function __clone() {
487 // Cloning instances of the class is forbidden.
488 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
489 }
490
491 /**
492 * Disable unserializing of the class
493 *
494 * @codeCoverageIgnore
495 * @access public
496 * @since 1.0.0
497 * @return void
498 */
499 public function __wakeup() {
500 // Unserializing instances of the class is forbidden.
501 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
502 }
503
504 }
505