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

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