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

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