PluginProbe
Autoptimize / trunk
Autoptimize vtrunk
3.1.16 2.2.2 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.5.0 2.5.1 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 All 108 releases
autoptimize / classes / autoptimizeImages.php

autoptimizeImages.php in Autoptimize trunk, at classes/autoptimizeImages.php

1,658 lines 78.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles optimizing images.
4 */
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit;
8 }
9
10 class autoptimizeImages
11 {
12 /**
13 * Options.
14 *
15 * @var array
16 */
17 protected $options = array();
18
19 /**
20 * Singleton instance.
21 *
22 * @var self|null
23 */
24 protected static $instance = null;
25
26 /**
27 * lazyload counter.
28 *
29 * @var int
30 */
31 protected $lazyload_counter = 0;
32
33 public function __construct( array $options = array() )
34 {
35 // If options are not provided, fetch them.
36 if ( empty( $options ) ) {
37 $options = $this->fetch_options();
38 }
39
40 $this->set_options( $options );
41 }
42
43 public function set_options( array $options )
44 {
45 $this->options = $options;
46
47 return $this;
48 }
49
50 public static function fetch_options()
51 {
52 $value = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_settings' );
53 if ( empty( $value ) ) {
54 // Fallback to returning defaults when no stored option exists yet.
55 $value = autoptimizeConfig::get_ao_imgopt_default_options();
56 }
57
58 // get service availability and add it to the options-array.
59 $value['availabilities'] = autoptimizeOptionWrapper::get_option( 'autoptimize_service_availablity' );
60
61 if ( empty( $value['availabilities'] ) || ! is_array( $value['availabilities'] ) ) {
62 $value['availabilities'] = null;
63
64 if ( true === autoptimizeImages::imgopt_active() ) {
65 $value['availabilities'] = autoptimizeUtils::check_service_availability( true );
66 }
67
68 if ( null === $value['availabilities'] ) {
69 // We can't seem to check service availability, use mock result with imgopt status UP.
70 $_mock_settings = array(
71 'extra_imgopt' => array(
72 'status' => 'up',
73 'hosts' => array(
74 '1' => 'https://sp-ao.shortpixel.ai/',
75 ),
76 ),
77 'critcss' => array(
78 'status' => 'up',
79 ),
80 );
81 $value['availabilities'] = $_mock_settings;
82 }
83 }
84
85 return $value;
86 }
87
88 public static function imgopt_active()
89 {
90 // function to quickly check if imgopt is active, used below but also in
91 // autoptimizeMain.php to start ob_ even if no HTML, JS or CSS optimizing is done
92 // and does not use/ request the availablity data (which could slow things down).
93 static $imgopt_active = null;
94
95 if ( null === $imgopt_active ) {
96 $opts = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_settings', '' );
97 if ( ! empty( $opts ) && is_array( $opts ) && array_key_exists( 'autoptimize_imgopt_checkbox_field_1', $opts ) && ! empty( $opts['autoptimize_imgopt_checkbox_field_1'] ) && '1' === $opts['autoptimize_imgopt_checkbox_field_1'] ) {
98 $imgopt_active = true;
99 } else {
100 $imgopt_active = false;
101 }
102 }
103
104 return $imgopt_active;
105 }
106
107 /**
108 * Helper for getting a singleton instance. While being an
109 * anti-pattern generally, it comes in handy for now from a
110 * readability/maintainability perspective, until we get some
111 * proper dependency injection going.
112 *
113 * @return self
114 */
115 public static function instance()
116 {
117 if ( null === self::$instance ) {
118 self::$instance = new self();
119 }
120
121 return self::$instance;
122 }
123
124 public function run()
125 {
126 if ( is_admin() ) {
127 if ( is_multisite() && is_network_admin() && autoptimizeOptionWrapper::is_ao_active_for_network() ) {
128 add_action( 'network_admin_menu', array( $this, 'imgopt_admin_menu' ) );
129 } else {
130 add_action( 'admin_menu', array( $this, 'imgopt_admin_menu' ) );
131 }
132 add_filter( 'autoptimize_filter_settingsscreen_tabs', array( $this, 'add_imgopt_tab' ), 9 );
133 } else {
134 add_action( 'wp', array( $this, 'run_on_frontend' ) );
135 }
136 }
137
138 public function run_on_frontend() {
139 if ( ! $this->should_run() ) {
140 if ( $this->should_lazyload() ) {
141 add_filter(
142 'wp_lazy_loading_enabled',
143 array( $this, 'should_disable_core_lazyload' ),
144 10,
145 3
146 );
147 add_filter(
148 'autoptimize_html_after_minify',
149 array( $this, 'filter_lazyload_images' ),
150 10,
151 1
152 );
153 add_action(
154 'wp_footer',
155 array( $this, 'add_lazyload_js_footer' ),
156 10,
157 0
158 );
159 }
160 return;
161 }
162
163 $active = false;
164
165 if ( apply_filters( 'autoptimize_filter_imgopt_do', true ) ) {
166 add_filter(
167 'autoptimize_html_after_minify',
168 array( $this, 'filter_optimize_images' ),
169 10,
170 1
171 );
172 $active = true;
173 }
174
175 if ( apply_filters( 'autoptimize_filter_imgopt_do_css', true ) ) {
176 // fixme: also act on already minified CSS!
177 add_filter(
178 'autoptimize_filter_base_replace_cdn',
179 array( $this, 'filter_optimize_css_images' ),
180 10,
181 1
182 );
183
184 add_filter(
185 'autoptimize_html_after_minify',
186 array( $this, 'filter_optimize_inline_css_images' ),
187 10,
188 1
189 );
190
191 $active = true;
192 }
193
194 if ( $active ) {
195 add_filter(
196 'autoptimize_extra_filter_tobepreconn',
197 array( $this, 'filter_preconnect_imgopt_url' ),
198 10,
199 1
200 );
201 }
202
203 if ( $this->should_lazyload() ) {
204 add_filter(
205 'wp_lazy_loading_enabled',
206 array( $this, 'should_disable_core_lazyload' ),
207 10,
208 3
209 );
210 add_action(
211 'wp_footer',
212 array( $this, 'add_lazyload_js_footer' ),
213 10,
214 0
215 );
216 }
217 }
218
219 /**
220 * Disables core's native lazyload for images, not for iframes.
221 *
222 * @param bool $flag Incoming flag (mostly true).
223 * @param string $tag Tag (img or iframe).
224 * @param string $context Full context.
225 *
226 * @return bool
227 */
228 public function should_disable_core_lazyload( $flag = true, $tag = '', $context = '' ) {
229 if ( 'img' === $tag ) {
230 return false;
231 }
232 return $flag;
233 }
234
235 /**
236 * Basic checks before we can run.
237 *
238 * @return bool
239 */
240 protected function should_run()
241 {
242 $opts = $this->options;
243 $service_not_down = ( 'down' !== $opts['availabilities']['extra_imgopt']['status'] );
244 $not_launch_status = ( 'launch' !== $opts['availabilities']['extra_imgopt']['status'] );
245
246 $do_cdn = true;
247 $_userstatus = $this->get_imgopt_provider_userstatus();
248 if ( isset( $_userstatus['Status'] ) && ( -2 == $_userstatus['Status'] || -3 == $_userstatus['Status'] ) ) {
249 // don't even attempt to put images on CDN if heavily exceeded threshold or if site not reachable.
250 $do_cdn = false;
251 }
252
253 if (
254 $this->imgopt_active()
255 && $do_cdn
256 && $service_not_down
257 && ( $not_launch_status || $this->launch_ok() )
258 ) {
259 return true;
260 }
261 return false;
262 }
263
264 public function get_imgopt_host()
265 {
266 static $imgopt_host = null;
267
268 if ( null === $imgopt_host ) {
269 $imgopt_host = 'https://sp-ao.shortpixel.ai/';
270 $avail_imgopt = $this->options['availabilities']['extra_imgopt'];
271 if ( ! empty( $avail_imgopt ) && array_key_exists( 'hosts', $avail_imgopt ) && is_array( $avail_imgopt['hosts'] ) ) {
272 $imgopt_host = array_rand( array_flip( $avail_imgopt['hosts'] ) );
273 }
274 $imgopt_host = apply_filters( 'autoptimize_filter_imgopt_host', $imgopt_host );
275 }
276
277 return $imgopt_host;
278 }
279
280 public static function get_imgopt_host_wrapper()
281 {
282 // needed for CI tests.
283 $self = new self();
284 return $self->get_imgopt_host();
285 }
286
287 public static function get_service_url_suffix()
288 {
289 $suffix = '/af/ZQXTBY0109483/' . AUTOPTIMIZE_SITE_DOMAIN;
290
291 return $suffix;
292 }
293
294 public function get_img_quality_string()
295 {
296 static $quality = null;
297
298 if ( null === $quality ) {
299 $q_array = $this->get_img_quality_array();
300 $setting = $this->get_img_quality_setting();
301 $quality = apply_filters(
302 'autoptimize_filter_imgopt_quality',
303 'q_' . $q_array[ $setting ]
304 );
305 }
306
307 return $quality;
308 }
309
310 public function get_img_quality_array()
311 {
312 static $map = null;
313
314 if ( null === $map ) {
315 $map = array(
316 '1' => 'lossy',
317 '2' => 'glossy',
318 '3' => 'lossless',
319 );
320 $map = apply_filters(
321 'autoptimize_filter_imgopt_quality_array',
322 $map
323 );
324 }
325
326 return $map;
327 }
328
329 public function get_img_quality_setting()
330 {
331 static $q = null;
332
333 if ( null === $q ) {
334 if ( is_array( $this->options ) && array_key_exists( 'autoptimize_imgopt_select_field_2', $this->options ) ) {
335 $setting = $this->options['autoptimize_imgopt_select_field_2'];
336 }
337
338 if ( ! isset( $setting ) || empty( $setting ) || ( '1' !== $setting && '3' !== $setting ) ) {
339 // default image opt. value is 2 ("glossy").
340 $q = '2';
341 } else {
342 $q = $setting;
343 }
344 }
345
346 return $q;
347 }
348
349 public function filter_preconnect_imgopt_url( array $in )
350 {
351 $url_parts = parse_url( $this->get_imgopt_base_url() );
352 $in[] = $url_parts['scheme'] . '://' . $url_parts['host'];
353
354 return $in;
355 }
356
357 /**
358 * Makes sure given url contains the full scheme and hostname
359 * in case they're not present already.
360 *
361 * @param string $in Image url to normalize.
362 *
363 * @return string
364 */
365 private function normalize_img_url( $in )
366 {
367 // Only parse the site url once.
368 static $parsed_site_url = null;
369 if ( null === $parsed_site_url ) {
370 $parsed_site_url = parse_url( site_url() );
371 }
372
373 // get CDN domain once.
374 static $cdn_domain = null;
375 if ( is_null( $cdn_domain ) ) {
376 $cdn_url = $this->get_cdn_url();
377 if ( ! empty( $cdn_url ) ) {
378 $cdn_domain = parse_url( $cdn_url, PHP_URL_HOST );
379 } else {
380 $cdn_domain = '';
381 }
382 }
383
384 /**
385 * This method gets called a lot, often for identical urls it seems.
386 * `filter_optimize_css_images()` calls us, uses the resulting url and
387 * gives it to `can_optimize_image()`, and if that returns trueish
388 * then `build_imgopt_url()` is called (which, again, calls this method).
389 * Until we dig deeper into whether this all must really happen that
390 * way, having an internal cache here helps (to avoid doing repeated
391 * identical string operations).
392 */
393 static $cache = null;
394 if ( null === $cache ) {
395 $cache = array();
396 }
397
398 // Do the work on cache miss only.
399 if ( ! isset( $cache[ $in ] ) ) {
400 // Default to (the trimmed version of) what was given to us.
401 $result = trim( $in );
402
403 // Some silly plugins wrap background images in html-encoded quotes, so remove those from the img url.
404 $result = $this->fix_silly_bgimg_quotes( $result );
405
406 if ( autoptimizeUtils::is_protocol_relative( $result ) ) {
407 $result = $parsed_site_url['scheme'] . ':' . $result;
408 } elseif ( 0 === strpos( $result, '/' ) ) {
409 // Root-relative...
410 $result = $parsed_site_url['scheme'] . '://' . $parsed_site_url['host'] . $result;
411 } elseif ( ! empty( $cdn_domain ) && false === strpos( $this->get_imgopt_host(), $cdn_domain ) && strpos( $result, $cdn_domain ) !== 0 ) {
412 // remove CDN except if it is the image optimization one.
413 $result = str_replace( $cdn_domain, $parsed_site_url['host'], $result );
414 }
415
416 // filter (default off) to remove QS from image URL's to avoid eating away optimization credits.
417 if ( apply_filters( 'autoptimize_filter_imgopt_no_querystring', false ) && strpos( $result, '?' ) !== false ) {
418 $result = strtok( $result, '?' );
419 }
420
421 $result = apply_filters( 'autoptimize_filter_imgopt_normalized_url', $result );
422
423 // Store in cache.
424 $cache[ $in ] = $result;
425 }
426
427 return $cache[ $in ];
428 }
429
430 public function filter_optimize_css_images( $in )
431 {
432 $in = $this->normalize_img_url( $in );
433
434 if ( $this->can_optimize_image( $in ) && false === strpos( $in, $this->get_imgopt_host() ) ) {
435 return $this->build_imgopt_url( $in, '', '' );
436 } else {
437 return $in;
438 }
439 }
440
441 public function filter_optimize_inline_css_images( $html ) {
442 preg_match_all( '#<style[^>]*>([^<]*)</style>#Um', $html, $inline_css_blocks, PREG_SET_ORDER );
443 foreach ( $inline_css_blocks as $inline_css_block ) {
444 if ( false !== strpos( $inline_css_block[0], 'background' ) ) {
445 $inline_css_block_new = $this->replace_background_img_css( $inline_css_block[0] );
446 if ( $inline_css_block_new !== $inline_css_block[0] ) {
447 $html = str_replace( $inline_css_block[0], $inline_css_block_new, $html );
448 }
449 }
450 }
451 return $html;
452 }
453
454 public static function replace_background_img_css( $css ) {
455 // fixme; can/ should we cache these?
456 preg_match_all( '#background[^;}]*url\((.*)\)#Ui', $css, $backgrounds, PREG_SET_ORDER );
457 if ( is_array( $backgrounds ) && ! empty( $backgrounds ) ) {
458 foreach ( $backgrounds as $background ) {
459 if ( autoptimizeImages::can_optimize_image_wrapper( $background[1] ) ) {
460 $css = str_replace( $background[1], autoptimizeImages::build_imgopt_url_wrapper( $background[1] ), $css );
461 }
462 }
463 }
464 return $css;
465 }
466
467 private function get_imgopt_base_url()
468 {
469 static $imgopt_base_url = null;
470
471 if ( null === $imgopt_base_url ) {
472 $imgopt_host = $this->get_imgopt_host();
473 $quality = $this->get_img_quality_string();
474 $ret_val = apply_filters( 'autoptimize_filter_imgopt_wait', 'ret_img' ); // values: ret_wait, ret_img, ret_json, ret_blank.
475 if ( $this->should_ngimg() ) {
476 $sp_to_string = 'to_auto';
477 } else {
478 $sp_to_string = 'to_webp';
479 }
480 $sp_to_string = apply_filters( 'autoptimize_filter_imgopt_format', $sp_to_string ); // values: empty (= jpeg), to_webp (smart; webp or fallback), to_avif (avif or fallback) or to_auto (smart avif, webp or fallback).
481 $imgopt_base_url = $imgopt_host . 'client/' . $sp_to_string . ',' . $quality . ',' . $ret_val;
482 $imgopt_base_url = apply_filters( 'autoptimize_filter_imgopt_base_url', $imgopt_base_url );
483 }
484
485 return $imgopt_base_url;
486 }
487
488 public static function can_optimize_image_wrapper( $url, $tag = '', $testing = false ) {
489 $self = new self();
490 return $self->can_optimize_image( $url, $tag = '', $testing = false );
491 }
492
493 private function can_optimize_image( $url, $tag = '', $testing = false )
494 {
495 static $cdn_url = null;
496 static $nopti_images = null;
497
498 if ( null === $cdn_url ) {
499 $cdn_url = apply_filters(
500 'autoptimize_filter_base_cdnurl',
501 autoptimizeOptionWrapper::get_option( 'autoptimize_cdn_url', '' )
502 );
503 }
504
505 if ( null === $nopti_images || $testing ) {
506 if ( is_array( $this->options ) && array_key_exists( 'autoptimize_imgopt_text_field_6', $this->options ) ) {
507 $nopti_images = $this->options['autoptimize_imgopt_text_field_6'];
508 }
509 $nopti_images = apply_filters( 'autoptimize_filter_imgopt_noptimize', $nopti_images );
510 }
511
512 $site_host = AUTOPTIMIZE_SITE_DOMAIN;
513 $url = $this->normalize_img_url( $url );
514 $url_parsed = parse_url( $url );
515
516 if ( false === is_array( $url_parsed ) ) {
517 return false;
518 } elseif ( array_key_exists( 'host', $url_parsed ) && $url_parsed['host'] !== $site_host && empty( $cdn_url ) ) {
519 return false;
520 } elseif ( autoptimizeUtils::is_local_server() ) {
521 return false;
522 } elseif ( ! empty( $cdn_url ) && strpos( $url, $cdn_url ) === false && array_key_exists( 'host', $url_parsed ) && $url_parsed['host'] !== $site_host ) {
523 return false;
524 } elseif ( strpos( $url, '.php' ) !== false ) {
525 return false;
526 } elseif ( false === array_key_exists( 'path', $url_parsed ) || str_ireplace( array( '.png', '.gif', '.jpg', '.jpeg', '.webp', '.avif' ), '', $url_parsed['path'] ) === $url_parsed['path'] ) {
527 // fixme: better check against end of string.
528 return false;
529 } elseif ( ! empty( $nopti_images ) ) {
530 $nopti_images_array = array_filter( array_map( 'trim', explode( ',', $nopti_images ) ) );
531 foreach ( $nopti_images_array as $nopti_image ) {
532 if ( strpos( $url, $nopti_image ) !== false || ( ( '' !== $tag && strpos( $tag, $nopti_image ) !== false ) ) ) {
533 return false;
534 }
535 }
536 }
537 return true;
538 }
539
540 // wrapper for reuse in AOPro.
541 public static function build_imgopt_url_wrapper( $orig_url, $width = 0, $height = 0 ) {
542 $self = new self();
543 return $self->build_imgopt_url( $orig_url, $width = 0, $height = 0 );
544 }
545
546 private function build_imgopt_url( $orig_url, $width = 0, $height = 0 )
547 {
548 // sanitize width and height.
549 if ( strpos( $width, '%' ) !== false ) {
550 $width = 0;
551 }
552 if ( strpos( $height, '%' ) !== false ) {
553 $height = 0;
554 }
555 $width = (int) $width;
556 $height = (int) $height;
557
558 $filtered_url = apply_filters(
559 'autoptimize_filter_imgopt_build_url',
560 $orig_url,
561 $width,
562 $height
563 );
564
565 // If filter modified the url, return that.
566 if ( $filtered_url !== $orig_url ) {
567 return $filtered_url;
568 }
569
570 $normalized_url = $this->normalize_img_url( $orig_url );
571
572 // if the URL is ascii we check if we have a real URL with filter_var (which only works on ascii url's) and if not a real URL we return the original one.
573 if ( apply_filters( 'autoptimize_filter_imgopt_check_normalized_url', true ) && ! preg_match( '/[^\x20-\x7e]/', $normalized_url ) && false === filter_var( $normalized_url, FILTER_VALIDATE_URL ) ) {
574 return $orig_url;
575 }
576
577 $imgopt_base_url = $this->get_imgopt_base_url();
578 $imgopt_size = '';
579
580 if ( $width && 0 !== $width ) {
581 $imgopt_size = ',w_' . $width;
582 }
583
584 if ( $height && 0 !== $height ) {
585 $imgopt_size .= ',h_' . $height;
586 }
587
588 $url = $imgopt_base_url . $imgopt_size . '/' . $normalized_url;
589 $url = apply_filters( 'autoptimize_filter_imgopt_after_build_imgopt_url', $url );
590
591 return $url;
592 }
593
594 public function replace_data_thumbs( $matches )
595 {
596 return $this->replace_img_callback( $matches, 150, 150 );
597 }
598
599 public function replace_img_callback( $matches, $width = 0, $height = 0 )
600 {
601 $_normalized_img_url = $this->normalize_img_url( $matches[1] );
602 if ( $this->can_optimize_image( $matches[1], $matches[0] ) ) {
603 return str_replace( $matches[1], $this->build_imgopt_url( $_normalized_img_url, $width, $height ), $matches[0] );
604 } else {
605 return $matches[0];
606 }
607 }
608
609 public function replace_icon_callback( $matches )
610 {
611 if ( array_key_exists( '2', $matches ) ) {
612 $sizes = explode( 'x', $matches[2] );
613 $width = $sizes[0];
614 $height = $sizes[1];
615 } else {
616 $width = 180;
617 $height = 180;
618 }
619
620 // make sure we're not trying to optimize a *.ico file.
621 if ( strpos( $matches[1], '.ico' ) === false ) {
622 return $this->replace_img_callback( $matches, $width, $height );
623 } else {
624 return $matches[0];
625 }
626 }
627
628 public function filter_optimize_images( $in, $testing = false )
629 {
630 /*
631 * potential future functional improvements:
632 *
633 * filter for critical CSS.
634 */
635 $to_replace = array();
636 $to_preload = '';
637
638 // hide (no)script tags to avoid replacing (and potentially breaking) images in script tags.
639 if ( apply_filters( 'autoptimize_filter_imgopt_hide_script', true ) || $this->should_lazyload() ) {
640 $in = autoptimizeBase::replace_contents_with_marker_if_exists(
641 'SCRIPT',
642 '<script',
643 '#<(?:no)?script.*?<\/(?:no)?script>#is',
644 $in
645 );
646 }
647
648 // get img preloads as set in post metabox, exploding ", " instead of "," because LCP preload
649 // could be a shortpixel URL, which has comma's and results in way too many preloads.
650 $metabox_preloads = array_filter( array_map( 'trim', explode( ', ', wp_strip_all_tags( autoptimizeConfig::get_post_meta_ao_settings( 'ao_post_preload' ) ) ) ) );
651 $metabox_preloads = apply_filters( 'autoptimize_filter_images_metabox_preloads', $metabox_preloads );
652
653 // extract img tags.
654 if ( preg_match_all( '#<img[^>]*src[^>]*>#Usmi', $in, $matches ) ) {
655 foreach ( $matches[0] as $tag ) {
656 $tag = apply_filters( 'autoptimize_filter_imgopt_tag_preopt', $tag );
657
658 $orig_tag = $tag;
659 $imgopt_w = '';
660 $imgopt_h = '';
661
662 // first do (data-)srcsets.
663 if ( preg_match_all( '#srcset=("|\')(.*)("|\')#Usmi', $tag, $allsrcsets, PREG_SET_ORDER ) ) {
664 foreach ( $allsrcsets as $srcset ) {
665 $srcset = $srcset[2];
666 $orig_srcset = $srcset;
667 $srcsets = explode( ',', $srcset );
668 foreach ( $srcsets as $indiv_srcset ) {
669 $indiv_srcset_parts = explode( ' ', trim( $indiv_srcset ) );
670 if ( isset( $indiv_srcset_parts[1] ) && rtrim( $indiv_srcset_parts[1], 'w' ) !== $indiv_srcset_parts[1] ) {
671 $imgopt_w = rtrim( $indiv_srcset_parts[1], 'w' );
672 }
673 if ( $this->can_optimize_image( $indiv_srcset_parts[0], $tag, $testing ) && false === apply_filters( 'autoptimize_filter_imgopt_do_spai', false ) ) {
674 $imgopt_url = $this->build_imgopt_url( $indiv_srcset_parts[0], $imgopt_w, '' );
675 $srcset = str_replace( $indiv_srcset_parts[0], $imgopt_url, $srcset );
676 }
677 }
678 $tag = str_replace( $orig_srcset, $srcset, $tag );
679 }
680 }
681
682 // proceed with img src.
683 // get width and height and add to $imgopt_size.
684 $_get_size = $this->get_size_from_tag( $tag );
685 $imgopt_w = $_get_size['width'];
686 $imgopt_h = $_get_size['height'];
687
688 // then start replacing images src.
689 $tag = autoptimizeAttributeParser::modify_attributes( $tag, 'img', '/src$/i', function( $name, $url ) use ( $tag, $testing, $imgopt_w, $imgopt_h ) {
690 // 1. Skip if empty or a data-URI
691 if ( ! $url || strpos( $url, 'data:' ) === 0 ) {
692 return $url;
693 }
694
695 // 2. Run Autoptimize logic
696 if ( $this->can_optimize_image( $url, $tag, $testing ) && ! apply_filters( 'autoptimize_filter_imgopt_do_spai', false ) ) {
697 return $this->build_imgopt_url( $url, $imgopt_w, $imgopt_h );
698 }
699
700 // 3. No changes needed
701 return $url;
702 }, false, true );
703
704 $_url = autoptimizeAttributeParser::get( $tag, 'img', '/src$/i' );
705 $url = $_url['src'];
706
707 // check if the image needs to be prelaoded.
708 if ( ! empty( $metabox_preloads ) && is_array( $metabox_preloads ) && str_replace( $metabox_preloads, '', $tag ) !== $tag ) {
709 $to_preload .= $this->create_img_preload_tag( $tag );
710 }
711
712 // do lazyload stuff.
713 if ( $this->should_lazyload( $in ) && ! empty( $url ) ) {
714 // first do lpiq placeholder logic.
715 if ( strpos( $url, $this->get_imgopt_host() ) === 0 ) {
716 // if all img src have been replaced during srcset, we have to extract the
717 // origin url from the imgopt one to be able to set a lqip placeholder.
718 $_url = substr( $url, strpos( $url, '/http' ) + 1 );
719 } else {
720 $_url = $url;
721 }
722
723 $_url = $this->normalize_img_url( $_url );
724
725 $placeholder = '';
726 if ( $this->can_optimize_image( $_url, $tag ) && apply_filters( 'autoptimize_filter_imgopt_lazyload_dolqip', false, $_url ) && false === apply_filters( 'autoptimize_filter_imgopt_do_spai', false ) ) {
727 $lqip_w = '';
728 $lqip_h = '';
729 if ( isset( $imgopt_w ) && ! empty( $imgopt_w ) ) {
730 $lqip_w = ',w_' . $imgopt_w;
731 }
732 if ( isset( $imgopt_h ) && ! empty( $imgopt_h ) ) {
733 $lqip_h = ',h_' . $imgopt_h;
734 }
735 $placeholder = $this->get_imgopt_host() . 'client/q_lqip,ret_wait' . $lqip_w . $lqip_h . '/' . $_url;
736 }
737 // then call add_lazyload-function with lpiq placeholder if set.
738 $tag = $this->add_lazyload( $tag, $placeholder );
739 }
740
741 // add decoding="async" behind filter, not sure if I'll make it default true yet.
742 if ( true === apply_filters( 'autoptimize_filter_imgopt_add_decoding', true ) && false === strpos( $tag, ' decoding=' ) ) {
743 $tag = str_replace( '<img ', '<img decoding="async" ', $tag );
744 }
745
746 $tag = apply_filters( 'autoptimize_filter_imgopt_tag_postopt', $tag );
747
748 // and add tag to array for later replacement.
749 if ( $tag !== $orig_tag ) {
750 $to_replace[ $orig_tag ] = $tag;
751 }
752 }
753 }
754
755 // and replace all.
756 $out = str_replace( array_keys( $to_replace ), array_values( $to_replace ), $in );
757
758 // misc. node attributes that might hold image url's (incl. the previously separate data-thumb).
759 $extra_attr_with_img = apply_filters( 'autoptimize_filter_imgopt_attr_with_img', array( array( 'div', 'data-thumb'), array( 'div', 'data-background' ), array( 'img', 'data-retina' ) ) );
760 if ( ! empty( $extra_attr_with_img ) && is_array( $extra_attr_with_img ) ) {
761 foreach ( $extra_attr_with_img as $candidate ) {
762 if ( is_array( $candidate ) && strpos( $out, $candidate[1] ) !== false ) {
763 $_regex = '/\<' . $candidate[0] . '(?:[^>]*)?\s' . $candidate[1] . '=(?:"|\')(.+?)(?:"|\')(?:[^>]*)?>/s';
764 $out = preg_replace_callback(
765 $_regex,
766 array( $this, 'replace_img_callback' ),
767 $out
768 );
769 }
770 }
771 }
772
773 // background-image in inline style.
774 if ( ( strpos( $out, 'background-image:' ) !== false || strpos( $out, 'background:' ) !== false ) && strpos( $out, 'url(' ) !== false && apply_filters( 'autoptimize_filter_imgopt_backgroundimages', true ) ) {
775 $out = preg_replace_callback(
776 '/style=(?:"|\')[^<>]*?background(?:-image)?:[^;"\'()>]*url\((?:"|\')?([^"\')]*)(?:"|\')?\)/',
777 array( $this, 'replace_img_callback' ),
778 $out
779 );
780 }
781
782 // act on icon links.
783 if ( ( strpos( $out, '<link rel="icon"' ) !== false || ( strpos( $out, "<link rel='icon'" ) !== false ) ) && apply_filters( 'autoptimize_filter_imgopt_linkicon', true ) ) {
784 $out = preg_replace_callback(
785 '/<link\srel=(?:"|\')(?:apple-touch-)?icon(?:-precomposed)?(?:"|\').*\shref=(?:"|\')(.*)(?:"|\')(?:\ssizes=(?:"|\')(\d*x\d*)(?:"|\'))?\s?\/?>/Um',
786 array( $this, 'replace_icon_callback' ),
787 $out
788 );
789 }
790
791 // lazyload picture source tags and bgimage.
792 if ( $this->should_lazyload() ) {
793 $out = $this->process_picture_tag( $out, true, true );
794 $out = $this->process_bgimage( $out );
795 } else {
796 $out = $this->process_picture_tag( $out, true, false );
797 }
798
799 // restore (no)script tags.
800 if ( apply_filters( 'autoptimize_filter_imgopt_hide_script', true ) || $this->should_lazyload() ) {
801 $out = autoptimizeBase::restore_marked_content(
802 'SCRIPT',
803 $out
804 );
805 }
806
807 if ( ! empty( $metabox_preloads ) && is_array( $metabox_preloads ) && empty( $to_preload ) && false !== apply_filters( 'autoptimize_filter_imgopt_dopreloads', true ) ) {
808 // the preload was not in an img tag, so adding a non-responsive preload instead.
809 foreach ( $metabox_preloads as $img_preload ) {
810 $to_preload .= apply_filters( 'autoptimize_filter_imgopt_preload_tag_result', $this->kses_preload_link( '<link fetchpriority="high" rel="preload" href="' . $img_preload . '" as="image">' ) );
811 }
812 }
813
814 if ( ! empty( $to_preload ) ) {
815 $out = autoptimizeExtra::inject_preloads( $to_preload, $out );
816 }
817
818 return $out;
819 }
820
821 public static function get_size_from_tag( $tag ) {
822 // reusable function to extract widht and height from an image tag
823 // enforcing a filterable maximum width and height (default 4999X4999).
824 $width = '';
825 $height = '';
826
827 if ( preg_match( '#width=("|\')(.*)("|\')#Usmi', $tag, $_width ) ) {
828 if ( strpos( $_width[2], '%' ) === false ) {
829 $width = (int) $_width[2];
830 }
831 }
832 if ( preg_match( '#height=("|\')(.*)("|\')#Usmi', $tag, $_height ) ) {
833 if ( strpos( $_height[2], '%' ) === false ) {
834 $height = (int) $_height[2];
835 }
836 }
837
838 // check for and enforce (filterable) max sizes.
839 $_max_width = apply_filters( 'autoptimize_filter_imgopt_max_width', 4999 );
840 if ( $width > $_max_width ) {
841 $_width = $_max_width;
842 if ( ! empty( $height ) && is_int( $height ) ) {
843 $height = $_width / $width * $height;
844 }
845 $width = $_width;
846 }
847 $_max_height = apply_filters( 'autoptimize_filter_imgopt_max_height', 4999 );
848 if ( $height > $_max_height ) {
849 $_height = $_max_height;
850 if ( ! empty( $width ) && is_int( $width ) ) {
851 $width = $_height / $height * $width;
852 }
853 $height = $_height;
854 }
855
856 return array(
857 'width' => $width,
858 'height' => $height,
859 );
860 }
861
862 /**
863 * Lazyload functions
864 */
865 public static function should_lazyload_wrapper( $no_meta = false ) {
866 // needed in autoptimizeMain.php.
867 $self = new self();
868 return $self->should_lazyload( '', $no_meta );
869 }
870
871 public function should_lazyload( $context = '', $no_meta = false ) {
872 if ( ! empty( $this->options['autoptimize_imgopt_checkbox_field_3'] ) && false === $this->check_nolazy() ) {
873 $lazyload_return = true;
874 } else {
875 $lazyload_return = false;
876 }
877
878 // If page/ post check post_meta to see if lazyload is off for page.
879 if ( false === $no_meta && false === autoptimizeConfig::get_post_meta_ao_settings( 'ao_post_lazyload' ) ) {
880 $lazyload_return = false;
881 }
882
883 $lazyload_return = apply_filters( 'autoptimize_filter_imgopt_should_lazyload', $lazyload_return, $context );
884
885 return $lazyload_return;
886 }
887
888 public static function check_nolazy() {
889 if ( array_key_exists( 'ao_nolazy', $_GET ) && '1' === $_GET['ao_nolazy'] ) {
890 return true;
891 } else {
892 return false;
893 }
894 }
895
896 public function filter_lazyload_images( $in )
897 {
898 // only used is image optimization is NOT active but lazyload is.
899 $to_replace = array();
900 $to_preload = '';
901
902 // hide (no)script tags to avoid nesting noscript tags (as lazyloaded images add noscript).
903 $out = autoptimizeBase::replace_contents_with_marker_if_exists(
904 'SCRIPT',
905 '<script',
906 '#<(?:no)?script.*?<\/(?:no)?script>#is',
907 $in
908 );
909
910 // get img preloads as set in post metabox.
911 $metabox_preloads = array_filter( array_map( 'trim', explode( ',', wp_strip_all_tags( autoptimizeConfig::get_post_meta_ao_settings( 'ao_post_preload' ) ) ) ) );
912
913 // extract img tags and add lazyload attribs/ add preloads.
914 if ( preg_match_all( '#<img[^>]*src[^>]*>#Usmi', $out, $matches ) ) {
915 foreach ( $matches[0] as $tag ) {
916 // check if image needs to be preloaded.
917 if ( ! empty( $metabox_preloads ) && is_array( $metabox_preloads ) && str_replace( $metabox_preloads, '', $tag ) !== $tag ) {
918 $to_preload .= $this->create_img_preload_tag( $tag );
919 }
920
921 // and lazyloaded.
922 if ( $this->should_lazyload( $out ) ) {
923 $to_replace[ $tag ] = $this->add_lazyload( $tag );
924 }
925 }
926 $out = str_replace( array_keys( $to_replace ), array_values( $to_replace ), $out );
927 }
928
929 // and also lazyload picture tag.
930 $out = $this->process_picture_tag( $out, false, true );
931
932 // and inline style blocks with background-image.
933 $out = $this->process_bgimage( $out );
934
935 // restore noscript tags.
936 $out = autoptimizeBase::restore_marked_content(
937 'SCRIPT',
938 $out
939 );
940
941 if ( ! empty( $metabox_preloads ) && is_array( $metabox_preloads ) && empty( $to_preload ) && false !== apply_filters( 'autoptimize_filter_imgopt_dopreloads', true ) ) {
942 // the preload was not in an img tag, so adding a non-responsive preload instead.
943 foreach ( $metabox_preloads as $img_preload ) {
944 $to_preload .= apply_filters( 'autoptimize_filter_imgopt_preload_tag_result', $this->kses_preload_link( '<link fetchpriority="high" rel="preload" href="' . $img_preload . '" as="image">' ) );
945 }
946 }
947
948 if ( ! empty( $to_preload ) ) {
949 $out = autoptimizeExtra::inject_preloads( $to_preload, $out );
950 }
951
952 return $out;
953 }
954
955 public function add_lazyload( $tag, $placeholder = '' ) {
956 // adds actual lazyload-attributes to an image node.
957 $this->lazyload_counter++;
958
959 $_lazyload_from_nth = '';
960 if ( array_key_exists( 'autoptimize_imgopt_number_field_7', $this->options ) ) {
961 $_lazyload_from_nth = $this->options['autoptimize_imgopt_number_field_7'];
962 }
963 $_lazyload_from_nth = apply_filters( 'autoptimize_filter_imgopt_lazyload_from_nth', $_lazyload_from_nth );
964
965 if ( str_ireplace( $this->get_lazyload_exclusions(), '', $tag ) === $tag && $this->lazyload_counter >= $_lazyload_from_nth ) {
966 $tag = $this->maybe_fix_missing_quotes( $tag );
967
968 // store original tag for use in noscript version.
969 $noscript_tag = '<noscript>' . autoptimizeUtils::remove_id_from_node( $tag ) . '</noscript>';
970
971 $lazyload_class = apply_filters( 'autoptimize_filter_imgopt_lazyload_class', 'lazyload' );
972
973 // insert lazyload class.
974 $tag = $this->inject_classes_in_tag( $tag, "$lazyload_class " );
975
976 if ( ! $placeholder || empty( $placeholder ) ) {
977 // get image width & heigth for placeholder fun (and to prevent content reflow).
978 $_get_size = $this->get_size_from_tag( $tag );
979 $width = $_get_size['width'];
980 $height = $_get_size['height'];
981 if ( false === $width || empty( $width ) ) {
982 $width = 210; // default width for SVG placeholder.
983 }
984 if ( false === $height || empty( $height ) ) {
985 $height = $width / 3 * 2; // if no height, base it on width using the 3/2 aspect ratio.
986 }
987
988 // insert the actual lazyload stuff.
989 // see https://css-tricks.com/preventing-content-reflow-from-lazy-loaded-images/ for great read on why we're using empty svg's.
990 $placeholder = apply_filters( 'autoptimize_filter_imgopt_lazyload_placeholder', $this->get_default_lazyload_placeholder( $width, $height ) );
991 }
992
993 $tag = str_replace( ' src=', ' data-src=', $tag );
994 $tag = str_replace( ' srcset=', ' data-srcset=', $tag );
995 $tag = str_replace( '<img ', '<img src=\'' . $placeholder . '\' ', $tag );
996
997 // move sizes to data-sizes unless filter says no.
998 if ( apply_filters( 'autoptimize_filter_imgopt_lazyload_move_sizes', true ) ) {
999 $tag = str_replace( ' sizes=', ' data-sizes=', $tag );
1000 }
1001
1002 // add the noscript-tag from earlier.
1003 $tag = $noscript_tag . $tag;
1004 $tag = apply_filters( 'autoptimize_filter_imgopt_lazyloaded_img', $tag );
1005 } else {
1006 $tag = apply_filters( 'autoptimize_filter_imgopt_not_lazyloaded_img', $tag );
1007 }
1008
1009 return $tag;
1010 }
1011
1012 public function add_lazyload_js_footer() {
1013 if ( false === autoptimizeMain::should_buffer() || autoptimizeMain::is_amp_markup( '' ) ) {
1014 return;
1015 }
1016
1017 // The JS will by default be excluded form autoptimization but this can be changed with a filter.
1018 $noptimize_flag = '';
1019 if ( apply_filters( 'autoptimize_filter_imgopt_lazyload_js_noptimize', true ) ) {
1020 $noptimize_flag = ' data-noptimize="1"';
1021 }
1022
1023 $_extra = autoptimizeOptionWrapper::get_option( 'autoptimize_extra_settings', '' );
1024 if ( is_array( $_extra ) && array_key_exists( 'autoptimize_extra_checkbox_field_0', $_extra ) && ! empty( $_extra['autoptimize_extra_checkbox_field_0'] ) ) {
1025 // if "remove query strings" is active in "extra", then let's be consistant and not add one ourselves :-) ?
1026 $lazysizes_js = plugins_url( 'external/js/lazysizes.min.js', __FILE__ );
1027 } else {
1028 $lazysizes_js = plugins_url( 'external/js/lazysizes.min.js?ao_version=' . AUTOPTIMIZE_PLUGIN_VERSION, __FILE__ );
1029 }
1030
1031 $cdn_url = $this->get_cdn_url();
1032 if ( ! empty( $cdn_url ) ) {
1033 $cdn_url = rtrim( $cdn_url, '/' );
1034 $lazysizes_js = str_replace( AUTOPTIMIZE_WP_SITE_URL, $cdn_url, $lazysizes_js );
1035 }
1036
1037 $type_js = '';
1038 if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
1039 $type_js = ' type="text/javascript"';
1040 }
1041
1042 // Adds lazyload CSS & JS to footer, using echo because wp_enqueue_script seems not to support pushing attributes (async).
1043 echo apply_filters( 'autoptimize_filter_imgopt_lazyload_cssoutput', '<noscript><style>.lazyload{display:none;}</style></noscript>' );
1044 echo apply_filters( 'autoptimize_filter_imgopt_lazyload_jsconfig', '<script' . $type_js . $noptimize_flag . '>window.lazySizesConfig=window.lazySizesConfig||{};window.lazySizesConfig.loadMode=1;</script>' );
1045 echo apply_filters( 'autoptimize_filter_imgopt_lazyload_js', '<script async' . $type_js . $noptimize_flag . ' src=\'' . $lazysizes_js . '\'></script>' );
1046 }
1047
1048 public function create_img_preload_tag( $tag ) {
1049 if ( false === apply_filters( 'autoptimize_filter_imgopt_dopreloads', true ) ) {
1050 return '';
1051 }
1052
1053 // clean up; remove tabs/ linebreaks/ spaces.
1054 $tag = preg_replace( '/\s+/', ' ', $tag );
1055
1056 // remove noscript.
1057 if ( false !== strpos( $tag, '<noscript' ) ) {
1058 $tag = preg_replace( '/<noscript.*<\/noscript>/mU', '', $tag );
1059 }
1060
1061 // rewrite img tag to link preload img.
1062 $_from = array( '<img ', ' src=', ' sizes=', ' srcset=' );
1063 $_to = array( '<link fetchpriority="high" rel="preload" as="image" ', ' href=', ' imagesizes=', ' imagesrcset=' );
1064 $tag = str_replace( $_from, $_to, $tag );
1065
1066 // sanitize output
1067 $tag = $this->kses_preload_link( $tag );
1068
1069 // and provide filter for late changes.
1070 $tag = apply_filters( 'autoptimize_filter_imgopt_preload_tag_result', $tag );
1071
1072 return $tag;
1073 }
1074
1075 public function kses_preload_link( $_preload ) {
1076 // using kses, remove all unneeded attributes
1077 // keeping only those we *know* are OK and/ or needed.
1078 $allowed_html = array(
1079 'link' => array(
1080 'rel' => true,
1081 'as' => true,
1082 'href' => true,
1083 'imagesizes' => true,
1084 'imagesrcset' => true,
1085 'type' => true,
1086 'media' => true,
1087 'fetchpriority' => true,
1088 ),
1089 );
1090 $_preload = wp_kses( $_preload, $allowed_html );
1091
1092 return $_preload;
1093 }
1094
1095 public static function get_cdn_url() {
1096 // getting CDN url here to avoid having to make bigger changes to autoptimizeBase.
1097 static $cdn_url = null;
1098
1099 if ( null === $cdn_url ) {
1100 $cdn_url = autoptimizeOptionWrapper::get_option( 'autoptimize_cdn_url', '' );
1101 $cdn_url = autoptimizeUtils::tweak_cdn_url_if_needed( $cdn_url );
1102 $cdn_url = apply_filters( 'autoptimize_filter_base_cdnurl', $cdn_url );
1103 }
1104
1105 return $cdn_url;
1106 }
1107
1108 public function get_lazyload_exclusions() {
1109 // returns array of strings that if found in an <img tag will stop the img from being lazy-loaded.
1110 static $exclude_lazyload_array = null;
1111
1112 if ( null === $exclude_lazyload_array ) {
1113 $options = $this->options;
1114
1115 // set default exclusions.
1116 $exclude_lazyload_array = array( 'skip-lazy', 'data-no-lazy', 'notlazy', 'data-src', 'data-srcset', 'data:image/', 'data-lazyload', 'rev-slidebg', 'loading="eager"', 'fetchpriority="high"' );
1117
1118 // add from setting.
1119 if ( array_key_exists( 'autoptimize_imgopt_text_field_5', $options ) ) {
1120 $exclude_lazyload_option = $options['autoptimize_imgopt_text_field_5'];
1121 if ( ! empty( $exclude_lazyload_option ) ) {
1122 $exclude_lazyload_array = array_merge( $exclude_lazyload_array, array_filter( array_map( 'trim', explode( ',', $options['autoptimize_imgopt_text_field_5'] ) ) ) );
1123 }
1124 }
1125
1126 // and filter for developer-initiated changes.
1127 $exclude_lazyload_array = apply_filters( 'autoptimize_filter_imgopt_lazyload_exclude_array', $exclude_lazyload_array );
1128 }
1129
1130 return $exclude_lazyload_array;
1131 }
1132
1133 public function inject_classes_in_tag( $tag, $target_class ) {
1134 if ( strpos( $tag, 'class=' ) !== false ) {
1135 $tag = preg_replace( '/(\sclass\s?=\s?("|\'))/', '$1' . $target_class, $tag );
1136 } else {
1137 $tag = preg_replace( '/(<[a-zA-Z]*)\s/', '$1 class="' . trim( $target_class ) . '" ', $tag );
1138 }
1139
1140 return $tag;
1141 }
1142
1143 public function get_default_lazyload_placeholder( $imgopt_w, $imgopt_h ) {
1144 return 'data:image/svg+xml,%3Csvg%20xmlns=%22http://www.w3.org/2000/svg%22%20viewBox=%220%200%20' . $imgopt_w . '%20' . $imgopt_h . '%22%3E%3C/svg%3E';
1145 }
1146
1147 public function should_ngimg() {
1148 static $ngimg_return = null;
1149
1150 if ( is_null( $ngimg_return ) ) {
1151 // nextgen img only works if imgopt is active.
1152 if ( ! empty( $this->options['autoptimize_imgopt_checkbox_field_4'] ) && $this->imgopt_active() ) {
1153 $ngimg_return = true;
1154 } else {
1155 $ngimg_return = false;
1156 }
1157 }
1158
1159 return $ngimg_return;
1160 }
1161
1162 public function process_picture_tag( $in, $imgopt = false, $lazy = false ) {
1163 // check if "<picture" is present and if filter allows us to process <picture>.
1164 if ( strpos( $in, '<picture' ) === false || apply_filters( 'autoptimize_filter_imgopt_dopicture', true ) === false ) {
1165 return $in;
1166 }
1167
1168 $_exclusions = $this->get_lazyload_exclusions();
1169 $to_replace_pict = array();
1170
1171 // extract and process each picture-node.
1172 preg_match_all( '#<picture.*</picture>#Usmi', $in, $_pictures, PREG_SET_ORDER );
1173 foreach ( $_pictures as $_picture ) {
1174 $_picture = $this->maybe_fix_missing_quotes( $_picture );
1175 if ( strpos( $_picture[0], '<source ' ) !== false && preg_match_all( '#<source .*srcset=(?:"|\')(?!data)(.*)(?:"|\').*>#Usmi', $_picture[0], $_sources, PREG_SET_ORDER ) !== false ) {
1176 foreach ( $_sources as $_source ) {
1177 $_picture_replacement = $_source[0];
1178
1179 // should we optimize the image?
1180 if ( $imgopt && $this->can_optimize_image( $_source[1], $_picture[0] ) ) {
1181 $_picture_replacement = str_replace( $_source[1], $this->build_imgopt_url( $_source[1] ), $_picture_replacement );
1182 }
1183 // should we lazy-load?
1184 if ( $lazy && $this->should_lazyload() && str_ireplace( $_exclusions, '', $_picture_replacement ) === $_picture_replacement ) {
1185 $_picture_replacement = str_replace( ' srcset=', ' data-srcset=', $_picture_replacement );
1186 }
1187 $to_replace_pict[ $_source[0] ] = $_picture_replacement;
1188 }
1189 }
1190 }
1191
1192 // and return the fully procesed $in.
1193 $out = str_replace( array_keys( $to_replace_pict ), array_values( $to_replace_pict ), $in );
1194
1195 return $out;
1196 }
1197
1198 public function process_bgimage( $in ) {
1199 if ( strpos( $in, 'background-image:' ) !== false && apply_filters( 'autoptimize_filter_imgopt_lazyload_backgroundimages', true ) ) {
1200 $out = preg_replace_callback(
1201 '/(<(?:article|aside|body|div|footer|header|p|section|span|table)[^>]*)\sstyle=(?:"|\')[^<>]*?background-image:\s?url\((?:"|\')?([^"\')]*)(?:"|\')?\)[^>]*/',
1202 array( $this, 'lazyload_bgimg_callback' ),
1203 $in
1204 );
1205 return $out;
1206 }
1207 return $in;
1208 }
1209
1210 public function lazyload_bgimg_callback( $matches ) {
1211 if ( str_ireplace( $this->get_lazyload_exclusions(), '', $matches[0] ) === $matches[0] ) {
1212 // get placeholder & lazyload class strings.
1213 $placeholder = apply_filters( 'autoptimize_filter_imgopt_lazyload_placeholder', $this->get_default_lazyload_placeholder( 500, 300 ) );
1214 $lazyload_class = apply_filters( 'autoptimize_filter_imgopt_lazyload_class', 'lazyload' );
1215 // remove quotes from url() to be able to replace in next step.
1216 $out = str_replace( array( "url('" . $matches[2] . "')", 'url("' . $matches[2] . '")' ), 'url(' . $matches[2] . ')', $matches[0] );
1217 // replace background-image URL with SVG placeholder.
1218 $out = str_replace( 'url(' . $matches[2], 'url(' . $placeholder, $out );
1219 // sanitize bgimg src for quote sillyness.
1220 $bgimg_src = $this->fix_silly_bgimg_quotes( $matches[2] );
1221 // add data-bg attribute with real background-image URL for lazyload to pick up.
1222 $out = str_replace( $matches[1], $matches[1] . ' data-bg="' . $bgimg_src . '"', $out );
1223 // and finally add lazyload class to tag.
1224 $out = $this->inject_classes_in_tag( $out, "$lazyload_class " );
1225 return $out;
1226 }
1227 return $matches[0];
1228 }
1229
1230 public function fix_silly_bgimg_quotes( $tag_in ) {
1231 // some themes/ pagebuilders wrap backgroundimages in HTML-encoded quotes (or linebreaks) which breaks imgopt/ lazyloading, this removes them.
1232 return trim( str_replace( array( "\r\n", '"', '&quot;', '&#034;', '&apos;', '&#039;' ), '', $tag_in ) );
1233 }
1234
1235 public function maybe_fix_missing_quotes( $tag_in ) {
1236 // W3TC's Minify_HTML class removes quotes around attribute value, this re-adds them for the class and width/height attributes so we can lazyload properly.
1237 if ( ( file_exists( WP_PLUGIN_DIR . '/w3-total-cache/w3-total-cache.php' ) && class_exists( 'Minify_HTML' ) && apply_filters( 'autoptimize_filter_imgopt_fixquotes', true ) ) ) {
1238 $tag_out = autoptimizeAttributeParser::rebuild( $tag_in );
1239 return $tag_out;
1240 } else {
1241 return $tag_in;
1242 }
1243 }
1244
1245 /**
1246 * Admin page logic and related functions below.
1247 */
1248 public function imgopt_admin_menu()
1249 {
1250 // no acces if multisite and not network admin and no site config allowed.
1251 if ( autoptimizeConfig::should_show_menu_tabs() ) {
1252 add_submenu_page(
1253 '',
1254 'autoptimize_imgopt',
1255 'autoptimize_imgopt',
1256 'manage_options',
1257 'autoptimize_imgopt',
1258 array( $this, 'imgopt_options_page' )
1259 );
1260 }
1261 register_setting( 'autoptimize_imgopt_settings', 'autoptimize_imgopt_settings' );
1262 }
1263
1264 public function add_imgopt_tab( $in )
1265 {
1266 if ( autoptimizeConfig::should_show_menu_tabs() ) {
1267 $in = array_merge( $in, array( 'autoptimize_imgopt' => apply_filters( 'autoptimize_filter_imgopt_tab_text', esc_html__( 'Images', 'autoptimize' ) ) ) );
1268 }
1269
1270 return $in;
1271 }
1272
1273 public function imgopt_options_page()
1274 {
1275 // phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace
1276 // phpcs:disable Generic.Formatting.DisallowMultipleStatements.SameLine
1277
1278 // Check querystring for "refreshCacheChecker" and call cachechecker if so.
1279 if ( array_key_exists( 'refreshImgProvStats', $_GET ) && 1 == $_GET['refreshImgProvStats'] ) {
1280 $this->query_img_provider_stats( true );
1281 }
1282
1283 $options = $this->fetch_options();
1284 $sp_url_suffix = $this->get_service_url_suffix();
1285 ?>
1286 <style>
1287 #ao_settings_form {background: white;border: 1px solid #ccc;padding: 1px 15px;margin: 15px 10px 10px 0;}
1288 #ao_settings_form .form-table th {font-weight: normal;}
1289 #autoptimize_imgopt_descr{font-size: 120%;}
1290 #ao_settings_form td p.condition-warning {color: orange;}
1291 </style>
1292 <script>document.title = "Autoptimize: <?php esc_html_e( 'Images', 'autoptimize' ); ?> " + document.title;</script>
1293 <div class="wrap">
1294 <h1><?php apply_filters( 'autoptimize_filter_settings_is_pro', false ) ? esc_html_e( 'Autoptimize Pro Settings', 'autoptimize' ) : esc_html_e( 'Autoptimize Settings', 'autoptimize' ); ?></h1>
1295 <?php echo autoptimizeConfig::ao_admin_tabs(); ?>
1296 <?php if ( autoptimizeUtils::is_local_server() ) { ?>
1297 <div class="notice-warning notice"><p>
1298 <?php
1299 echo esc_html__( 'The image optimization service does not work on locally hosted sites or when the server is on a private network.', 'autoptimize' );
1300 ?>
1301 </p></div>
1302 <?php } ?>
1303 <?php if ( 'down' === $options['availabilities']['extra_imgopt']['status'] ) { ?>
1304 <div class="notice-warning notice"><p>
1305 <?php
1306 // translators: "Autoptimize support forum" will appear in a "a href".
1307 echo sprintf( esc_html__( 'The image optimization service is currently down, image optimization will be skipped until further notice. Check the %1$sAutoptimize support forum%2$s for more info.', 'autoptimize' ), '<a href="https://wordpress.org/support/plugin/autoptimize/" target="_blank">', '</a>' );
1308 ?>
1309 </p></div>
1310 <?php } ?>
1311
1312 <?php if ( 'launch' === $options['availabilities']['extra_imgopt']['status'] && ! autoptimizeImages::instance()->launch_ok() ) { ?>
1313 <div class="notice-warning notice"><p>
1314 <?php esc_html_e( 'The image optimization service is launching, but not yet available for this domain, it should become available in the next couple of days.', 'autoptimize' ); ?>
1315 </p></div>
1316 <?php } ?>
1317
1318 <?php if ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'get_active_modules' ) && in_array( 'photon', Jetpack::get_active_modules() ) ) { ?>
1319 <div class="notice-warning notice"><p>
1320 <?php
1321 // translators: "disable Jetpack's site accelerator for images" will appear in a "a href" linking to the jetpack settings page.
1322 echo sprintf( esc_html__( 'Please %1$sdisable Jetpack\'s site accelerator for images%2$s to be able to use Autoptomize\'s advanced image optimization features below.', 'autoptimize' ), '<a href="admin.php?page=jetpack#/settings">', '</a>' );
1323 ?>
1324 </p></div>
1325 <?php } ?>
1326 <form id='ao_settings_form' action='<?php echo admin_url( 'options.php' ); ?>' method='post'>
1327 <?php settings_fields( 'autoptimize_imgopt_settings' ); ?>
1328 <h2><?php esc_html_e( 'Image optimization', 'autoptimize' ); ?></h2>
1329 <span id='autoptimize_imgopt_descr'><?php echo apply_filters( 'autoptimize_filter_imgopt_intro_copy', esc_html__( 'Make your site significantly faster by simply ticking a few boxes and start serving CDN powered, optimized images in the next-gen WebP image format! No additional plugins or services needed.', 'autoptimize' ) ); ?></span>
1330 <table class="form-table">
1331 <tr>
1332 <th scope="row"><?php esc_html_e( 'Image optimization & CDN', 'autoptimize' ); ?></th>
1333 <td>
1334 <label><input id='autoptimize_imgopt_checkbox' type='checkbox' name='autoptimize_imgopt_settings[autoptimize_imgopt_checkbox_field_1]' <?php if ( ! empty( $options['autoptimize_imgopt_checkbox_field_1'] ) && '1' === $options['autoptimize_imgopt_checkbox_field_1'] ) { echo 'checked="checked"'; } ?> value='1'><?php echo apply_filters( 'autoptimize_filter_imgopt_main_setting_copy', esc_html__( 'On-the-fly image optimization and fast delivery via the Shortpixel global CDN.', 'autoptimize' ) ); ?></label>
1335 <?php
1336 // show shortpixel status.
1337 $_notice = autoptimizeImages::instance()->get_imgopt_status_notice();
1338 if ( $_notice ) {
1339 switch ( $_notice['status'] ) {
1340 case 2:
1341 $_notice_color = 'green';
1342 break;
1343 case 1:
1344 $_notice_color = 'orange';
1345 break;
1346 case -1:
1347 case -2:
1348 case -3:
1349 $_notice_color = 'red';
1350 break;
1351 default:
1352 $_notice_color = 'green';
1353 }
1354 echo apply_filters( 'autoptimize_filter_imgopt_settings_status', '<p><strong><span style="color:' . $_notice_color . ';">' . esc_html__( 'Shortpixel status: ', 'autoptimize' ) . '</span></strong>' . $_notice['notice'] . '</p>' );
1355 } else {
1356 // translators: link points to shortpixel.
1357 $upsell_msg_1 = '<p>' . sprintf( esc_html__( 'Get more Google love by speeding up your website. Start serving on-the-fly optimized images (also in the "next-gen" %4$sWebP%5$s image format) by %1$sShortPixel%2$s. No additional image optimization plugins are needed: your images are optimized, cached and served from %3$sShortPixel\'s global CDN%2$s.', 'autoptimize' ), '<a href="https://misc.optimizingmatters.com/partners/?from=aofree&partner=shortpixelupsell" target="_blank">', '</a>', '<a href="https://help.shortpixel.com/article/62-where-does-the-cdn-has-pops" target="_blank">', '<strong>', '</strong>' );
1358 if ( 'launch' === $options['availabilities']['extra_imgopt']['status'] ) {
1359 $upsell_msg_2 = sprintf( esc_html__( 'For a limited time only, this service is offered free for all Autoptimize users, %1$sdon\'t miss the chance to test it%2$s and see how much it could improve your site\'s speed.', 'autoptimize' ), '<strong>', '</strong>' );
1360 } else {
1361 // translators: 1st link points to autoptimize.com.pro, 2nd to shortpixel.
1362 $upsell_msg_2 = sprintf( esc_html__( 'For (nearly) %5$sunlimited image optimizations %1$sbuy Autoptimize Pro%2$s%6$s which also includes Critical CSS and extra "booster" options or %3$ssign up at Shortpixel%4$s.', 'autoptimize' ), '<a href="https://autoptimize.com/pro/" target="_blank">', '</a>', '<a href="https://misc.optimizingmatters.com/partners/?from=aofree&partner=shortpixelupsell" target="_blank">', '</a>', '<strong>', '</strong>' );
1363 }
1364 echo apply_filters( 'autoptimize_filter_imgopt_settings_copy', $upsell_msg_1 . ' ' . $upsell_msg_2 . '</p>' );
1365 }
1366 // translators: link points to shortpixel FAQ.
1367 $faqcopy = sprintf( esc_html__( '%3$sQuestions%4$s? Take a look at the %1$sAutoptimize + ShortPixel FAQ%2$s!', 'autoptimize' ), '<strong><a href="https://help.shortpixel.com/category/405-autoptimize" target="_blank">', '</strong></a>', '<strong>', '</strong>' );
1368 $faqcopy = $faqcopy . ' ' . esc_html__( 'Only works for websites and images that are publicly available.', 'autoptimize' );
1369 // translators: links points to shortpixel TOS & Privacy Policy.
1370 $toscopy = sprintf( esc_html__( 'Usage of this feature is subject to Shortpixel\'s %1$sTerms of Use%2$s and %3$sPrivacy policy%4$s.', 'autoptimize' ), '<a href="https://shortpixel.com/tos' . $sp_url_suffix . '" target="_blank">', '</a>', '<a href="https://shortpixel.com/privacy' . $sp_url_suffix . '" target="_blank">', '</a>' );
1371 echo apply_filters( 'autoptimize_filter_imgopt_settings_tos', '<p>' . $faqcopy . ' ' . $toscopy . '</p>' );
1372 ?>
1373 </td>
1374 </tr>
1375 <tr id='autoptimize_imgopt_optimization_exclusions' <?php if ( ! array_key_exists( 'autoptimize_imgopt_checkbox_field_1', $options ) || ( isset( $options['autoptimize_imgopt_checkbox_field_1'] ) && '1' !== $options['autoptimize_imgopt_checkbox_field_1'] ) ) { echo 'class="hidden"'; } ?>>
1376 <th scope="row"><?php esc_html_e( 'Optimization exclusions', 'autoptimize' ); ?></th>
1377 <td>
1378 <label><input type='text' style='width:80%' id='autoptimize_imgopt_optimization_exclusions' name='autoptimize_imgopt_settings[autoptimize_imgopt_text_field_6]' value='<?php if ( ! empty( $options['autoptimize_imgopt_text_field_6'] ) ) { echo esc_attr( $options['autoptimize_imgopt_text_field_6'] ); } ?>'><br /><?php esc_html_e( 'Comma-separated list of image classes or filenames that should not be optimized.', 'autoptimize' ); ?></label>
1379 </td>
1380 </tr>
1381 <tr id='autoptimize_imgopt_quality' <?php if ( ! array_key_exists( 'autoptimize_imgopt_checkbox_field_1', $options ) || ( isset( $options['autoptimize_imgopt_checkbox_field_1'] ) && '1' !== $options['autoptimize_imgopt_checkbox_field_1'] ) ) { echo 'class="hidden"'; } ?>>
1382 <th scope="row"><?php esc_html_e( 'Image Optimization quality', 'autoptimize' ); ?></th>
1383 <td>
1384 <label>
1385 <select name='autoptimize_imgopt_settings[autoptimize_imgopt_select_field_2]'>
1386 <?php
1387 $_imgopt_array = autoptimizeImages::instance()->get_img_quality_array();
1388 $_imgopt_val = autoptimizeImages::instance()->get_img_quality_setting();
1389
1390 foreach ( $_imgopt_array as $key => $value ) {
1391 echo '<option value="' . $key . '"';
1392 if ( $_imgopt_val == $key ) {
1393 echo ' selected';
1394 }
1395 echo '>' . ucfirst( $value ) . '</option>';
1396 }
1397 echo "\n";
1398 ?>
1399 </select>
1400 </label>
1401 <p>
1402 <?php
1403 // translators: link points to shortpixel image test page.
1404 echo apply_filters( 'autoptimize_filter_imgopt_quality_copy', sprintf( esc_html__( 'You can %1$stest compression levels here%2$s.', 'autoptimize' ), '<a href="https://shortpixel.com/online-image-compression' . $sp_url_suffix . '" target="_blank">', '</a>' ) );
1405 ?>
1406 </p>
1407 </td>
1408 </tr>
1409 <?php
1410 if ( apply_filters( 'autoptimize_filter_imgopt_settings_show_avif', false ) || $this->should_ngimg() ) {
1411 ?>
1412 <tr id='autoptimize_imgopt_ngimg' <?php if ( ! array_key_exists( 'autoptimize_imgopt_checkbox_field_1', $options ) || ( isset( $options['autoptimize_imgopt_checkbox_field_1'] ) && '1' !== $options['autoptimize_imgopt_checkbox_field_1'] ) ) { echo 'class="hidden"'; } ?>>
1413 <th scope="row">
1414 <?php esc_html_e( 'Load AVIF in supported browsers?', 'autoptimize' ); ?>
1415 </th>
1416 <td>
1417 <p class="condition-warning">
1418 <?php
1419 if ( '3' === autoptimizeImages::instance()->get_img_quality_setting() && $this->should_ngimg() ) {
1420 _e( 'Your Image optimization quality is set to "lossless", but AVIF files can end up being bigger than jpeg or webp for lossless images, so consider choosing a different quality or disable the AVIF option.', 'autoptimize' );
1421 }
1422 ?>
1423 </p>
1424 <label><input type='checkbox' id='autoptimize_imgopt_ngimg_checkbox' name='autoptimize_imgopt_settings[autoptimize_imgopt_checkbox_field_4]' <?php if ( ! empty( $options['autoptimize_imgopt_checkbox_field_4'] ) && '1' === $options['autoptimize_imgopt_checkbox_field_4'] ) { echo 'checked="checked"'; } ?> value='1'>
1425 <?php esc_html_e( 'Automatically serve AVIF image format to any browser that supports it.', 'autoptimize' ); ?> <?php printf( esc_html__( '%1$sImportant caveat%2$s; although it %5$scan result in even smaller image sizes for lossy images%4$s it should be taken into account that %3$s%1$sAVIF can require more resources%2$s to decode/ display%4$s and as such %1$smight be slower%2$s on low-end (mobile) devices.', 'autoptimize' ), '<strong>', '</strong>', '<a href="https://storage.googleapis.com/avif-comparison/decode-timing.html" target="_blank">', '</a>', '<a href="https://tsev.dev/posts/2023-11-10-should-avif-be-the-dominant-image-format/" target="_blank">' ); ?>
1426 </label>
1427 </td>
1428 </tr>
1429 <?php
1430 } else {
1431 ?>
1432 <input type='hidden' id='autoptimize_imgopt_ngimg_checkbox' name='autoptimize_imgopt_settings[autoptimize_imgopt_checkbox_field_4]' value='0'>
1433 <?php
1434 }
1435 ?>
1436 <tr>
1437 <th scope="row"><?php esc_html_e( 'Lazy-load images?', 'autoptimize' ); ?></th>
1438 <td>
1439 <label><input type='checkbox' id='autoptimize_imgopt_lazyload_checkbox' name='autoptimize_imgopt_settings[autoptimize_imgopt_checkbox_field_3]' <?php if ( ! empty( $options['autoptimize_imgopt_checkbox_field_3'] ) && '1' === $options['autoptimize_imgopt_checkbox_field_3'] ) { echo 'checked="checked"'; } ?> value='1'><?php esc_html_e( 'Image lazy-loading will delay the loading of non-visible images to allow the browser to optimally load all resources for the "above the fold"-page first.', 'autoptimize' ); ?></label>
1440 </td>
1441 </tr>
1442 <tr id='autoptimize_imgopt_lazyload_exclusions' <?php if ( ! array_key_exists( 'autoptimize_imgopt_checkbox_field_3', $options ) || ( isset( $options['autoptimize_imgopt_checkbox_field_3'] ) && '1' !== $options['autoptimize_imgopt_checkbox_field_3'] ) ) { echo 'class="autoptimize_lazyload_child hidden"'; } else { echo 'class="autoptimize_lazyload_child"'; } ?>>
1443 <th scope="row"><?php esc_html_e( 'Lazy-load exclusions', 'autoptimize' ); ?></th>
1444 <td>
1445 <label><input type='text' style='width:80%' id='autoptimize_imgopt_lazyload_exclusions_text' name='autoptimize_imgopt_settings[autoptimize_imgopt_text_field_5]' value='<?php if ( ! empty( $options['autoptimize_imgopt_text_field_5'] ) ) { echo esc_attr( $options['autoptimize_imgopt_text_field_5'] ); } ?>'><br /><?php esc_html_e( 'Comma-separated list of to be excluded image classes or filenames.', 'autoptimize' ); ?></label>
1446 </td>
1447 </tr>
1448 <tr id='autoptimize_imgopt_lazyload_from_nth_image' <?php if ( ! array_key_exists( 'autoptimize_imgopt_checkbox_field_3', $options ) || ( isset( $options['autoptimize_imgopt_checkbox_field_3'] ) && '1' !== $options['autoptimize_imgopt_checkbox_field_3'] ) ) { echo 'class="autoptimize_lazyload_child hidden"'; } else { echo 'class="autoptimize_lazyload_child"'; } ?>>
1449 <th scope="row"><?php esc_html_e( 'Lazy-load from nth image', 'autoptimize' ); ?></th>
1450 <td>
1451 <label><input type='number' min='0' max='50' style='width:80%' id='autoptimize_imgopt_lazyload_from_nth_image_number' name='autoptimize_imgopt_settings[autoptimize_imgopt_number_field_7]' value='<?php if ( ! empty( $options['autoptimize_imgopt_number_field_7'] ) ) { echo esc_attr( $options['autoptimize_imgopt_number_field_7'] ); } else { echo '1'; } ?>'><br /><?php esc_html_e( 'Don\'t lazyload the first X images, \'1\' lazyloads all.', 'autoptimize' ); ?></label>
1452 </td>
1453 </tr>
1454 </table>
1455 <p class="submit"><input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Save Changes', 'autoptimize' ); ?>" /></p>
1456 </form>
1457 <script>
1458 jQuery(document).ready(function() {
1459 jQuery("#autoptimize_imgopt_checkbox").change(function() {
1460 if (this.checked) {
1461 jQuery("#autoptimize_imgopt_quality").show("slow");
1462 jQuery("#autoptimize_imgopt_ngimg").show("slow");
1463 jQuery("#autoptimize_imgopt_optimization_exclusions").show("slow");
1464 } else {
1465 jQuery("#autoptimize_imgopt_quality").hide("slow");
1466 jQuery("#autoptimize_imgopt_ngimg").hide("slow");
1467 jQuery("#autoptimize_imgopt_optimization_exclusions").hide("slow");
1468 }
1469 });
1470 jQuery("#autoptimize_imgopt_lazyload_checkbox").change(function() {
1471 if (this.checked) {
1472 jQuery(".autoptimize_lazyload_child").show("slow");
1473 } else {
1474 jQuery(".autoptimize_lazyload_child").hide("slow");
1475 }
1476 });
1477 });
1478 </script>
1479 <?php
1480 }
1481
1482 /**
1483 * Ïmg opt status as used on dashboard.
1484 */
1485 public function get_imgopt_status_notice() {
1486 if ( $this->imgopt_active() && apply_filters( 'autoptimize_filter_imgopt_status_shortpixel', true ) ) {
1487 $_imgopt_notice = '';
1488 $_stat = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_provider_stat', '' );
1489 $_site_host = AUTOPTIMIZE_SITE_DOMAIN;
1490 $_imgopt_upsell = 'https://misc.optimizingmatters.com/partners/?from=aofree&partner=shortpixelupsell';
1491 $_imgopt_assoc = 'https://shortpixel.helpscoutdocs.com/article/94-how-to-associate-a-domain-to-my-account';
1492 $_imgopt_unreach = 'https://shortpixel.helpscoutdocs.com/article/148-why-are-my-images-redirected-from-cdn-shortpixel-ai';
1493
1494 if ( is_array( $_stat ) ) {
1495 if ( 1 == $_stat['Status'] ) {
1496 // translators: "add more credits" will appear in a "a href".
1497 $_imgopt_notice = sprintf( esc_html__( 'Your ShortPixel image optimization and CDN quota is almost used, make sure you %1$sadd more credits%2$s to avoid slowing down your website %4$sor consider using %3$sAutoptimize Pro%2$s which comes with (nearly) unlimited image optimization%5$s but also automated critical CSS and extra booster options.', 'autoptimize' ), '<a href="' . $_imgopt_upsell . '" target="_blank">', '</a>', '<a href="https://autoptimize.com/pro/" target="_blank">', '<strong>', '</strong>' );
1498 } elseif ( -1 == $_stat['Status'] || -2 == $_stat['Status'] ) {
1499 // translators: "add more credits" will appear in a "a href".
1500 $_imgopt_notice = sprintf( esc_html__( 'Your ShortPixel image optimization and CDN quota has been exhausted, %1$sadd more credits%2$s to continue to quickly deliver optimized images on your website %4$sor consider using %3$sAutoptimize Pro%2$s which comes with (nearly) unlimited image optimization%5$s but also automated critical CSS and extra booster options.', 'autoptimize' ), '<a href="' . $_imgopt_upsell . '" target="_blank">', '</a>', '<a href="https://autoptimize.com/pro/" target="_blank">', '<strong>', '</strong>' );
1501 // translators: "associate your domain" will appear in a "a href".
1502 $_imgopt_notice = $_imgopt_notice . ' ' . sprintf( esc_html__( 'If you have enough CDN quota remaining, then you may need to %1$sassociate your domain%2$s to your Shortpixel account.', 'autoptimize' ), '<a rel="noopener noreferrer" href="' . $_imgopt_assoc . '" target="_blank">', '</a>' );
1503 } elseif ( -3 == $_stat['Status'] ) {
1504 // translators: "check the documentation here" will appear in a "a href".
1505 $_imgopt_notice = sprintf( esc_html__( 'It seems ShortPixel image optimization is not able to fetch images from your site, %1$scheck the documentation here%2$s for more information', 'autoptimize' ), '<a href="' . $_imgopt_unreach . '" target="_blank">', '</a>' );
1506 } else {
1507 $_imgopt_upsell = 'https://misc.optimizingmatters.com/partners/?from=aofree&partner=shortpixelupsell';
1508 // translators: "log in to check your account" will appear in a "a href".
1509 $_imgopt_notice = sprintf( esc_html__( 'Your ShortPixel image optimization and CDN quota are in good shape, %1$slog in to check your account%2$s.', 'autoptimize' ), '<a href="' . $_imgopt_upsell . '" target="_blank">', '</a>' );
1510 }
1511
1512 // add info on freshness + refresh link if status is not 2 (good shape).
1513 if ( 2 != $_stat['Status'] ) {
1514 $_imgopt_stats_refresh_url = add_query_arg(
1515 array(
1516 'page' => 'autoptimize_imgopt',
1517 'refreshImgProvStats' => '1',
1518 ),
1519 admin_url( 'options-general.php' )
1520 );
1521 if ( $_stat && array_key_exists( 'timestamp', $_stat ) && ! empty( $_stat['timestamp'] ) ) {
1522 $_imgopt_stats_last_run = esc_html__( 'based on status at ', 'autoptimize' ) . date_i18n( autoptimizeOptionWrapper::get_option( 'time_format' ), $_stat['timestamp'] );
1523 } else {
1524 $_imgopt_stats_last_run = esc_html__( 'based on previously fetched data', 'autoptimize' );
1525 }
1526 $_imgopt_notice .= ' (' . $_imgopt_stats_last_run . ', ';
1527 // translators: "here to refresh" links to the Autoptimize Extra page and forces a refresh of the img opt stats.
1528 $_imgopt_notice .= sprintf( esc_html__( 'you can click %1$shere to refresh your quota status%2$s', 'autoptimize' ), '<a href="' . $_imgopt_stats_refresh_url . '">', '</a>).' );
1529 }
1530
1531 // and make the full notice filterable.
1532 $_imgopt_notice = apply_filters( 'autoptimize_filter_imgopt_notice', $_imgopt_notice );
1533
1534 return array(
1535 'status' => $_stat['Status'],
1536 'notice' => $_imgopt_notice,
1537 );
1538 }
1539 }
1540 return false;
1541 }
1542
1543 public static function get_imgopt_status_notice_wrapper() {
1544 // needed for notice being shown in autoptimizeCacheChecker.php.
1545 $self = new self();
1546 return $self->get_imgopt_status_notice();
1547 }
1548
1549 /**
1550 * Get img provider stats (used to display notice).
1551 *
1552 * @param bool $_refresh Should the stats be forcefully refreshed or not.
1553 */
1554 public function query_img_provider_stats( $_refresh = false ) {
1555 if ( ! empty( $this->options['autoptimize_imgopt_checkbox_field_1'] ) && apply_filters( 'autoptimize_filter_imgopt_status_shortpixel', true ) ) {
1556 $url = '';
1557 $stat_dom = 'https://no-cdn.shortpixel.ai/';
1558 $endpoint = $stat_dom . 'read-domain/';
1559 $domain = AUTOPTIMIZE_SITE_DOMAIN;
1560
1561 // make sure parse_url result makes sense, keeping $url empty if not.
1562 if ( $domain && ! empty( $domain ) ) {
1563 $url = $endpoint . $domain;
1564 if ( true === $_refresh ) {
1565 $url = $url . '/refresh';
1566 }
1567 }
1568
1569 $url = apply_filters(
1570 'autoptimize_filter_imgopt_stat_url',
1571 $url
1572 );
1573
1574 // only do the remote call if $url is not empty to make sure no parse_url
1575 // weirdness results in useless calls.
1576 if ( ! empty( $url ) ) {
1577 $response = wp_remote_get( $url );
1578 if ( ! is_wp_error( $response ) ) {
1579 if ( '200' == wp_remote_retrieve_response_code( $response ) ) {
1580 $stats = json_decode( wp_remote_retrieve_body( $response ), true );
1581 autoptimizeOptionWrapper::update_option( 'autoptimize_imgopt_provider_stat', $stats );
1582 }
1583 }
1584 }
1585 }
1586 }
1587
1588 public static function get_img_provider_stats()
1589 {
1590 // wrapper around query_img_provider_stats() so we can get to $this->options from cronjob() in autoptimizeCacheChecker.
1591 $self = new self();
1592 return $self->query_img_provider_stats();
1593 }
1594
1595 /**
1596 * Determines and returns the service launch status.
1597 *
1598 * @return bool
1599 */
1600 public function launch_ok()
1601 {
1602 static $launch_status = null;
1603
1604 if ( null === $launch_status ) {
1605 $avail_imgopt = '';
1606 if ( is_array( $this->options ) && array_key_exists( 'availabilities', $this->options ) && is_array( $this->options['availabilities'] ) && array_key_exists( 'extra_imgopt', $this->options['availabilities'] ) ) {
1607 $avail_imgopt = $this->options['availabilities']['extra_imgopt'];
1608 }
1609
1610 $magic_number = intval( substr( md5( parse_url( AUTOPTIMIZE_WP_SITE_URL, PHP_URL_HOST ) ), 0, 3 ), 16 );
1611 $has_launched = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_launched', '' );
1612 $launch_status = false;
1613 if ( $has_launched || ( is_array( $avail_imgopt ) && array_key_exists( 'launch-threshold', $avail_imgopt ) && $magic_number < $avail_imgopt['launch-threshold'] ) ) {
1614 $launch_status = true;
1615 if ( ! $has_launched ) {
1616 autoptimizeOptionWrapper::update_option( 'autoptimize_imgopt_launched', 'on' );
1617 }
1618 }
1619 }
1620
1621 return $launch_status;
1622 }
1623
1624 public static function launch_ok_wrapper() {
1625 // needed for "plug" notice in autoptimizeMain.php.
1626 $self = new self();
1627 return $self->launch_ok();
1628 }
1629
1630 public function get_imgopt_provider_userstatus() {
1631 static $_provider_userstatus = null;
1632
1633 if ( is_null( $_provider_userstatus ) ) {
1634 $_stat = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_provider_stat', '' );
1635 if ( is_array( $_stat ) ) {
1636 if ( array_key_exists( 'Status', $_stat ) ) {
1637 $_provider_userstatus['Status'] = $_stat['Status'];
1638 } else {
1639 // if no stats then we assume all is well.
1640 $_provider_userstatus['Status'] = 2;
1641 }
1642 if ( array_key_exists( 'timestamp', $_stat ) ) {
1643 $_provider_userstatus['timestamp'] = $_stat['timestamp'];
1644 } else {
1645 // if no timestamp then we return "".
1646 $_provider_userstatus['timestamp'] = '';
1647 }
1648 } else {
1649 // no provider_stat yet, assume/ return all OK.
1650 $_provider_userstatus['Status'] = 2;
1651 $_provider_userstatus['timestamp'] = '';
1652 }
1653 }
1654
1655 return $_provider_userstatus;
1656 }
1657 }
1658