PluginProbe
Autoptimize / 2.8.3
Autoptimize v2.8.3
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 2.7.8 All 107 releases
autoptimize / classes / autoptimizeImages.php

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

1,404 lines 65.6 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 public function __construct( array $options = array() )
27 {
28 // If options are not provided, fetch them.
29 if ( empty( $options ) ) {
30 $options = $this->fetch_options();
31 }
32
33 $this->set_options( $options );
34 $this->lazyload_counter = 0;
35 }
36
37 public function set_options( array $options )
38 {
39 $this->options = $options;
40
41 return $this;
42 }
43
44 public static function fetch_options()
45 {
46 $value = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_settings' );
47 if ( empty( $value ) ) {
48 // Fallback to returning defaults when no stored option exists yet.
49 $value = autoptimizeConfig::get_ao_imgopt_default_options();
50 }
51
52 // get service availability and add it to the options-array.
53 $value['availabilities'] = autoptimizeOptionWrapper::get_option( 'autoptimize_service_availablity' );
54
55 if ( empty( $value['availabilities'] ) ) {
56 $value['availabilities'] = autoptimizeUtils::check_service_availability( true );
57 }
58
59 return $value;
60 }
61
62 public static function imgopt_active()
63 {
64 // function to quickly check if imgopt is active, used below but also in
65 // autoptimizeMain.php to start ob_ even if no HTML, JS or CSS optimizing is done
66 // and does not use/ request the availablity data (which could slow things down).
67 static $imgopt_active = null;
68
69 if ( null === $imgopt_active ) {
70 $opts = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_settings', '' );
71 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'] ) {
72 $imgopt_active = true;
73 } else {
74 $imgopt_active = false;
75 }
76 }
77
78 return $imgopt_active;
79 }
80
81 /**
82 * Helper for getting a singleton instance. While being an
83 * anti-pattern generally, it comes in handy for now from a
84 * readability/maintainability perspective, until we get some
85 * proper dependency injection going.
86 *
87 * @return self
88 */
89 public static function instance()
90 {
91 if ( null === self::$instance ) {
92 self::$instance = new self();
93 }
94
95 return self::$instance;
96 }
97
98 public function run()
99 {
100 if ( is_admin() ) {
101 if ( is_multisite() && is_network_admin() && autoptimizeOptionWrapper::is_ao_active_for_network() ) {
102 add_action( 'network_admin_menu', array( $this, 'imgopt_admin_menu' ) );
103 } else {
104 add_action( 'admin_menu', array( $this, 'imgopt_admin_menu' ) );
105 }
106 add_filter( 'autoptimize_filter_settingsscreen_tabs', array( $this, 'add_imgopt_tab' ), 9 );
107 } else {
108 add_action( 'wp', array( $this, 'run_on_frontend' ) );
109 }
110 }
111
112 public function run_on_frontend() {
113 if ( ! $this->should_run() ) {
114 if ( $this->should_lazyload() ) {
115 add_filter(
116 'wp_lazy_loading_enabled',
117 '__return_false'
118 );
119 add_filter(
120 'autoptimize_html_after_minify',
121 array( $this, 'filter_lazyload_images' ),
122 10,
123 1
124 );
125 add_action(
126 'wp_footer',
127 array( $this, 'add_lazyload_js_footer' ),
128 10,
129 0
130 );
131 }
132 return;
133 }
134
135 $active = false;
136
137 if ( apply_filters( 'autoptimize_filter_imgopt_do', true ) ) {
138 add_filter(
139 'autoptimize_html_after_minify',
140 array( $this, 'filter_optimize_images' ),
141 10,
142 1
143 );
144 $active = true;
145 }
146
147 if ( apply_filters( 'autoptimize_filter_imgopt_do_css', true ) ) {
148 add_filter(
149 'autoptimize_filter_base_replace_cdn',
150 array( $this, 'filter_optimize_css_images' ),
151 10,
152 1
153 );
154 $active = true;
155 }
156
157 if ( $active ) {
158 add_filter(
159 'autoptimize_extra_filter_tobepreconn',
160 array( $this, 'filter_preconnect_imgopt_url' ),
161 10,
162 1
163 );
164 }
165
166 if ( $this->should_lazyload() ) {
167 add_filter(
168 'wp_lazy_loading_enabled',
169 '__return_false'
170 );
171 add_action(
172 'wp_footer',
173 array( $this, 'add_lazyload_js_footer' ),
174 10,
175 0
176 );
177 }
178 }
179
180 /**
181 * Basic checks before we can run.
182 *
183 * @return bool
184 */
185 protected function should_run()
186 {
187 $opts = $this->options;
188 $service_not_down = ( 'down' !== $opts['availabilities']['extra_imgopt']['status'] );
189 $not_launch_status = ( 'launch' !== $opts['availabilities']['extra_imgopt']['status'] );
190
191 $do_cdn = true;
192 $_userstatus = $this->get_imgopt_provider_userstatus();
193 if ( isset( $_userstatus['Status'] ) && ( -2 == $_userstatus['Status'] || -3 == $_userstatus['Status'] ) ) {
194 // don't even attempt to put images on CDN if heavily exceeded threshold or if site not reachable.
195 $do_cdn = false;
196 }
197
198 if (
199 $this->imgopt_active()
200 && $do_cdn
201 && $service_not_down
202 && ( $not_launch_status || $this->launch_ok() )
203 ) {
204 return true;
205 }
206 return false;
207 }
208
209 public function get_imgopt_host()
210 {
211 static $imgopt_host = null;
212
213 if ( null === $imgopt_host ) {
214 $imgopt_host = 'https://cdn.shortpixel.ai/';
215 $avail_imgopt = $this->options['availabilities']['extra_imgopt'];
216 if ( ! empty( $avail_imgopt ) && array_key_exists( 'hosts', $avail_imgopt ) && is_array( $avail_imgopt['hosts'] ) ) {
217 $imgopt_host = array_rand( array_flip( $avail_imgopt['hosts'] ) );
218 }
219 $imgopt_host = apply_filters( 'autoptimize_filter_imgopt_host', $imgopt_host );
220 }
221
222 return $imgopt_host;
223 }
224
225 public static function get_imgopt_host_wrapper()
226 {
227 // needed for CI tests.
228 $self = new self();
229 return $self->get_imgopt_host();
230 }
231
232 public static function get_service_url_suffix()
233 {
234 $suffix = '/af/SPZURYE109483/' . AUTOPTIMIZE_SITE_DOMAIN;
235
236 return $suffix;
237 }
238
239 public function get_img_quality_string()
240 {
241 static $quality = null;
242
243 if ( null === $quality ) {
244 $q_array = $this->get_img_quality_array();
245 $setting = $this->get_img_quality_setting();
246 $quality = apply_filters(
247 'autoptimize_filter_imgopt_quality',
248 'q_' . $q_array[ $setting ]
249 );
250 }
251
252 return $quality;
253 }
254
255 public function get_img_quality_array()
256 {
257 static $map = null;
258
259 if ( null === $map ) {
260 $map = array(
261 '1' => 'lossy',
262 '2' => 'glossy',
263 '3' => 'lossless',
264 );
265 $map = apply_filters(
266 'autoptimize_filter_imgopt_quality_array',
267 $map
268 );
269 }
270
271 return $map;
272 }
273
274 public function get_img_quality_setting()
275 {
276 static $q = null;
277
278 if ( null === $q ) {
279 if ( is_array( $this->options ) && array_key_exists( 'autoptimize_imgopt_select_field_2', $this->options ) ) {
280 $setting = $this->options['autoptimize_imgopt_select_field_2'];
281 }
282
283 if ( ! isset( $setting ) || empty( $setting ) || ( '1' !== $setting && '3' !== $setting ) ) {
284 // default image opt. value is 2 ("glossy").
285 $q = '2';
286 } else {
287 $q = $setting;
288 }
289 }
290
291 return $q;
292 }
293
294 public function filter_preconnect_imgopt_url( array $in )
295 {
296 $url_parts = parse_url( $this->get_imgopt_base_url() );
297 $in[] = $url_parts['scheme'] . '://' . $url_parts['host'];
298
299 return $in;
300 }
301
302 /**
303 * Makes sure given url contains the full scheme and hostname
304 * in case they're not present already.
305 *
306 * @param string $in Image url to normalize.
307 *
308 * @return string
309 */
310 private function normalize_img_url( $in )
311 {
312 // Only parse the site url once.
313 static $parsed_site_url = null;
314 if ( null === $parsed_site_url ) {
315 $parsed_site_url = parse_url( site_url() );
316 }
317
318 // get CDN domain once.
319 static $cdn_domain = null;
320 if ( is_null( $cdn_domain ) ) {
321 $cdn_url = $this->get_cdn_url();
322 if ( ! empty( $cdn_url ) ) {
323 $cdn_domain = parse_url( $cdn_url, PHP_URL_HOST );
324 } else {
325 $cdn_domain = '';
326 }
327 }
328
329 /**
330 * This method gets called a lot, often for identical urls it seems.
331 * `filter_optimize_css_images()` calls us, uses the resulting url and
332 * gives it to `can_optimize_image()`, and if that returns trueish
333 * then `build_imgopt_url()` is called (which, again, calls this method).
334 * Until we dig deeper into whether this all must really happen that
335 * way, having an internal cache here helps (to avoid doing repeated
336 * identical string operations).
337 */
338 static $cache = null;
339 if ( null === $cache ) {
340 $cache = array();
341 }
342
343 // Do the work on cache miss only.
344 if ( ! isset( $cache[ $in ] ) ) {
345 // Default to (the trimmed version of) what was given to us.
346 $result = trim( $in );
347
348 // Some silly plugins wrap background images in html-encoded quotes, so remove those from the img url.
349 $result = $this->fix_silly_bgimg_quotes( $result );
350
351 if ( autoptimizeUtils::is_protocol_relative( $result ) ) {
352 $result = $parsed_site_url['scheme'] . ':' . $result;
353 } elseif ( 0 === strpos( $result, '/' ) ) {
354 // Root-relative...
355 $result = $parsed_site_url['scheme'] . '://' . $parsed_site_url['host'] . $result;
356 } elseif ( ! empty( $cdn_domain ) && strpos( $result, $cdn_domain ) !== 0 ) {
357 $result = str_replace( $cdn_domain, $parsed_site_url['host'], $result );
358 }
359
360 // filter (default off) to remove QS from image URL's to avoid eating away optimization credits.
361 if ( apply_filters( 'autoptimize_filter_imgopt_no_querystring', false ) && strpos( $result, '?' ) !== false ) {
362 $result = strtok( $result, '?' );
363 }
364
365 $result = apply_filters( 'autoptimize_filter_imgopt_normalized_url', $result );
366
367 // Store in cache.
368 $cache[ $in ] = $result;
369 }
370
371 return $cache[ $in ];
372 }
373
374 public function filter_optimize_css_images( $in )
375 {
376 $in = $this->normalize_img_url( $in );
377
378 if ( $this->can_optimize_image( $in ) ) {
379 return $this->build_imgopt_url( $in, '', '' );
380 } else {
381 return $in;
382 }
383 }
384
385 private function get_imgopt_base_url()
386 {
387 static $imgopt_base_url = null;
388
389 if ( null === $imgopt_base_url ) {
390 $imgopt_host = $this->get_imgopt_host();
391 $quality = $this->get_img_quality_string();
392 $ret_val = apply_filters( 'autoptimize_filter_imgopt_wait', 'ret_img' ); // values: ret_wait, ret_img, ret_json, ret_blank.
393 $imgopt_base_url = $imgopt_host . 'client/' . $quality . ',' . $ret_val;
394 $imgopt_base_url = apply_filters( 'autoptimize_filter_imgopt_base_url', $imgopt_base_url );
395 }
396
397 return $imgopt_base_url;
398 }
399
400 private function can_optimize_image( $url, $tag = '', $testing = false )
401 {
402 static $cdn_url = null;
403 static $nopti_images = null;
404
405 if ( null === $cdn_url ) {
406 $cdn_url = apply_filters(
407 'autoptimize_filter_base_cdnurl',
408 autoptimizeOptionWrapper::get_option( 'autoptimize_cdn_url', '' )
409 );
410 }
411
412 if ( null === $nopti_images || $testing ) {
413 if ( is_array( $this->options ) && array_key_exists( 'autoptimize_imgopt_text_field_6', $this->options ) ) {
414 $nopti_images = $this->options['autoptimize_imgopt_text_field_6'];
415 }
416 $nopti_images = apply_filters( 'autoptimize_filter_imgopt_noptimize', $nopti_images );
417 }
418
419 $site_host = AUTOPTIMIZE_SITE_DOMAIN;
420 $url = $this->normalize_img_url( $url );
421 $url_parsed = parse_url( $url );
422
423 if ( array_key_exists( 'host', $url_parsed ) && $url_parsed['host'] !== $site_host && empty( $cdn_url ) ) {
424 return false;
425 } elseif ( ! empty( $cdn_url ) && strpos( $url, $cdn_url ) === false && array_key_exists( 'host', $url_parsed ) && $url_parsed['host'] !== $site_host ) {
426 return false;
427 } elseif ( strpos( $url, '.php' ) !== false ) {
428 return false;
429 } elseif ( str_ireplace( array( '.png', '.gif', '.jpg', '.jpeg', '.webp', '.avif' ), '', $url_parsed['path'] ) === $url_parsed['path'] ) {
430 // fixme: better check against end of string.
431 return false;
432 } elseif ( ! empty( $nopti_images ) ) {
433 $nopti_images_array = array_filter( array_map( 'trim', explode( ',', $nopti_images ) ) );
434 foreach ( $nopti_images_array as $nopti_image ) {
435 if ( strpos( $url, $nopti_image ) !== false || ( ( '' !== $tag && strpos( $tag, $nopti_image ) !== false ) ) ) {
436 return false;
437 }
438 }
439 }
440 return true;
441 }
442
443 private function build_imgopt_url( $orig_url, $width = 0, $height = 0 )
444 {
445 // sanitize width and height.
446 if ( strpos( $width, '%' ) !== false ) {
447 $width = 0;
448 }
449 if ( strpos( $height, '%' ) !== false ) {
450 $height = 0;
451 }
452 $width = (int) $width;
453 $height = (int) $height;
454
455 $filtered_url = apply_filters(
456 'autoptimize_filter_imgopt_build_url',
457 $orig_url,
458 $width,
459 $height
460 );
461
462 // If filter modified the url, return that.
463 if ( $filtered_url !== $orig_url ) {
464 return $filtered_url;
465 }
466
467 $normalized_url = $this->normalize_img_url( $orig_url );
468
469 // 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.
470 if ( apply_filters( 'autoptimize_filter_imgopt_check_normalized_url', true ) && ! preg_match( '/[^\x20-\x7e]/', $normalized_url ) && false === filter_var( $normalized_url, FILTER_VALIDATE_URL ) ) {
471 return $orig_url;
472 }
473
474 $imgopt_base_url = $this->get_imgopt_base_url();
475 $imgopt_size = '';
476
477 if ( $width && 0 !== $width ) {
478 $imgopt_size = ',w_' . $width;
479 }
480
481 if ( $height && 0 !== $height ) {
482 $imgopt_size .= ',h_' . $height;
483 }
484
485 $url = $imgopt_base_url . $imgopt_size . '/' . $normalized_url;
486
487 return $url;
488 }
489
490 public function replace_data_thumbs( $matches )
491 {
492 return $this->replace_img_callback( $matches, 150, 150 );
493 }
494
495 public function replace_img_callback( $matches, $width = 0, $height = 0 )
496 {
497 $_normalized_img_url = $this->normalize_img_url( $matches[1] );
498 if ( $this->can_optimize_image( $matches[1], $matches[0] ) ) {
499 return str_replace( $matches[1], $this->build_imgopt_url( $_normalized_img_url, $width, $height ), $matches[0] );
500 } else {
501 return $matches[0];
502 }
503 }
504
505 public function replace_icon_callback( $matches )
506 {
507 if ( array_key_exists( '2', $matches ) ) {
508 $sizes = explode( 'x', $matches[2] );
509 $width = $sizes[0];
510 $height = $sizes[1];
511 } else {
512 $width = 180;
513 $height = 180;
514 }
515
516 // make sure we're not trying to optimize a *.ico file
517 if ( strpos( $matches[1], '.ico' ) === false ) {
518 return $this->replace_img_callback( $matches, $width, $height );
519 } else {
520 return $matches[0];
521 }
522 }
523
524 public function filter_optimize_images( $in, $testing = false )
525 {
526 /*
527 * potential future functional improvements:
528 *
529 * filter for critical CSS.
530 */
531 $to_replace = array();
532
533 // hide noscript tags to avoid nesting noscript tags (as lazyloaded images add noscript).
534 if ( $this->should_lazyload() ) {
535 $in = autoptimizeBase::replace_contents_with_marker_if_exists(
536 'SCRIPT',
537 '<script',
538 '#<(?:no)?script.*?<\/(?:no)?script>#is',
539 $in
540 );
541 }
542
543 // extract img tags.
544 if ( preg_match_all( '#<img[^>]*src[^>]*>#Usmi', $in, $matches ) ) {
545 foreach ( $matches[0] as $tag ) {
546 $tag = apply_filters( 'autoptimize_filter_imgopt_tag_preopt' , $tag );
547
548 $orig_tag = $tag;
549 $imgopt_w = '';
550 $imgopt_h = '';
551
552 // first do (data-)srcsets.
553 if ( preg_match_all( '#srcset=("|\')(.*)("|\')#Usmi', $tag, $allsrcsets, PREG_SET_ORDER ) ) {
554 foreach ( $allsrcsets as $srcset ) {
555 $srcset = $srcset[2];
556 $orig_srcset = $srcset;
557 $srcsets = explode( ',', $srcset );
558 foreach ( $srcsets as $indiv_srcset ) {
559 $indiv_srcset_parts = explode( ' ', trim( $indiv_srcset ) );
560 if ( isset( $indiv_srcset_parts[1] ) && rtrim( $indiv_srcset_parts[1], 'w' ) !== $indiv_srcset_parts[1] ) {
561 $imgopt_w = rtrim( $indiv_srcset_parts[1], 'w' );
562 }
563 if ( $this->can_optimize_image( $indiv_srcset_parts[0], $tag, $testing ) ) {
564 $imgopt_url = $this->build_imgopt_url( $indiv_srcset_parts[0], $imgopt_w, '' );
565 $srcset = str_replace( $indiv_srcset_parts[0], $imgopt_url, $srcset );
566 }
567 }
568 $tag = str_replace( $orig_srcset, $srcset, $tag );
569 }
570 }
571
572 // proceed with img src.
573 // get width and height and add to $imgopt_size.
574 $_get_size = $this->get_size_from_tag( $tag );
575 $imgopt_w = $_get_size['width'];
576 $imgopt_h = $_get_size['height'];
577
578 // then start replacing images src.
579 if ( preg_match_all( '#src=(?:"|\')(?!data)(.*)(?:"|\')#Usmi', $tag, $urls, PREG_SET_ORDER ) ) {
580 foreach ( $urls as $url ) {
581 $full_src_orig = $url[0];
582 $url = $url[1];
583 if ( $this->can_optimize_image( $url, $tag, $testing ) ) {
584 $imgopt_url = $this->build_imgopt_url( $url, $imgopt_w, $imgopt_h );
585 $full_imgopt_src = str_replace( $url, $imgopt_url, $full_src_orig );
586 $tag = str_replace( $full_src_orig, $full_imgopt_src, $tag );
587 }
588 }
589 }
590
591 // do lazyload stuff.
592 if ( $this->should_lazyload( $in ) && ! empty( $url ) ) {
593 // first do lpiq placeholder logic.
594 if ( strpos( $url, $this->get_imgopt_host() ) === 0 ) {
595 // if all img src have been replaced during srcset, we have to extract the
596 // origin url from the imgopt one to be able to set a lqip placeholder.
597 $_url = substr( $url, strpos( $url, '/http' ) + 1 );
598 } else {
599 $_url = $url;
600 }
601
602 $_url = $this->normalize_img_url( $_url );
603
604 $placeholder = '';
605 if ( $this->can_optimize_image( $_url, $tag ) && apply_filters( 'autoptimize_filter_imgopt_lazyload_dolqip', true, $_url ) ) {
606 $lqip_w = '';
607 $lqip_h = '';
608 if ( isset( $imgopt_w ) && ! empty( $imgopt_w ) ) {
609 $lqip_w = ',w_' . $imgopt_w;
610 }
611 if ( isset( $imgopt_h ) && ! empty( $imgopt_h ) ) {
612 $lqip_h = ',h_' . $imgopt_h;
613 }
614 $placeholder = $this->get_imgopt_host() . 'client/q_lqip,ret_wait' . $lqip_w . $lqip_h . '/' . $_url;
615 }
616 // then call add_lazyload-function with lpiq placeholder if set.
617 $tag = $this->add_lazyload( $tag, $placeholder );
618 }
619
620 $tag = apply_filters( 'autoptimize_filter_imgopt_tag_postopt' , $tag );
621
622 // and add tag to array for later replacement.
623 if ( $tag !== $orig_tag ) {
624 $to_replace[ $orig_tag ] = $tag;
625 }
626 }
627 }
628
629 // and replace all.
630 $out = str_replace( array_keys( $to_replace ), array_values( $to_replace ), $in );
631
632 // img thumbnails in e.g. woocommerce.
633 if ( strpos( $out, 'data-thumb' ) !== false && apply_filters( 'autoptimize_filter_imgopt_datathumbs', true ) ) {
634 $out = preg_replace_callback(
635 '/\<div(?:[^>]?)\sdata-thumb\=(?:\"|\')(.+?)(?:\"|\')(?:[^>]*)?\>/s',
636 array( $this, 'replace_data_thumbs' ),
637 $out
638 );
639 }
640
641 // background-image in inline style.
642 if ( strpos( $out, 'background-image:' ) !== false && apply_filters( 'autoptimize_filter_imgopt_backgroundimages', true ) ) {
643 $out = preg_replace_callback(
644 '/style=(?:"|\')[^<>]*?background-image:\s?url\((?:"|\')?([^"\')]*)(?:"|\')?\)/',
645 array( $this, 'replace_img_callback' ),
646 $out
647 );
648 }
649
650 // act on icon links.
651 if ( ( strpos( $out, '<link rel="icon"' ) !== false || ( strpos( $out, "<link rel='icon'" ) !== false ) ) && apply_filters( 'autoptimize_filter_imgopt_linkicon', true ) ) {
652 $out = preg_replace_callback(
653 '/<link\srel=(?:"|\')(?:apple-touch-)?icon(?:"|\').*\shref=(?:"|\')(.*)(?:"|\')(?:\ssizes=(?:"|\')(\d*x\d*)(?:"|\'))?\s\/>/Um',
654 array( $this, 'replace_icon_callback' ),
655 $out
656 );
657 }
658
659 // lazyload: restore noscript tags + lazyload picture source tags and bgimage.
660 if ( $this->should_lazyload() ) {
661 $out = autoptimizeBase::restore_marked_content(
662 'SCRIPT',
663 $out
664 );
665
666 $out = $this->process_picture_tag( $out, true, true );
667 $out = $this->process_bgimage( $out );
668 } else {
669 $out = $this->process_picture_tag( $out, true, false );
670 }
671
672 return $out;
673 }
674
675 public function get_size_from_tag( $tag ) {
676 // reusable function to extract widht and height from an image tag
677 // enforcing a filterable maximum width and height (default 4999X4999).
678 $width = '';
679 $height = '';
680
681 if ( preg_match( '#width=("|\')(.*)("|\')#Usmi', $tag, $_width ) ) {
682 if ( strpos( $_width[2], '%' ) === false ) {
683 $width = (int) $_width[2];
684 }
685 }
686 if ( preg_match( '#height=("|\')(.*)("|\')#Usmi', $tag, $_height ) ) {
687 if ( strpos( $_height[2], '%' ) === false ) {
688 $height = (int) $_height[2];
689 }
690 }
691
692 // check for and enforce (filterable) max sizes.
693 $_max_width = apply_filters( 'autoptimize_filter_imgopt_max_width', 4999 );
694 if ( $width > $_max_width ) {
695 $_width = $_max_width;
696 $height = $_width / $width * $height;
697 $width = $_width;
698 }
699 $_max_height = apply_filters( 'autoptimize_filter_imgopt_max_height', 4999 );
700 if ( $height > $_max_height ) {
701 $_height = $_max_height;
702 $width = $_height / $height * $width;
703 $height = $_height;
704 }
705
706 return array(
707 'width' => $width,
708 'height' => $height,
709 );
710 }
711
712 /**
713 * Lazyload functions
714 */
715 public static function should_lazyload_wrapper() {
716 // needed in autoptimizeMain.php.
717 $self = new self();
718 return $self->should_lazyload();
719 }
720
721 public function should_lazyload( $context = '' ) {
722 if ( ! empty( $this->options['autoptimize_imgopt_checkbox_field_3'] ) && false === $this->check_nolazy() ) {
723 $lazyload_return = true;
724 } else {
725 $lazyload_return = false;
726 }
727 $lazyload_return = apply_filters( 'autoptimize_filter_imgopt_should_lazyload', $lazyload_return, $context );
728
729 return $lazyload_return;
730 }
731
732 public function check_nolazy() {
733 if ( array_key_exists( 'ao_nolazy', $_GET ) && '1' === $_GET['ao_nolazy'] ) {
734 return true;
735 } else {
736 return false;
737 }
738 }
739
740 public function filter_lazyload_images( $in )
741 {
742 // only used is image optimization is NOT active but lazyload is.
743 $to_replace = array();
744
745 // hide (no)script tags to avoid nesting noscript tags (as lazyloaded images add noscript).
746 $out = autoptimizeBase::replace_contents_with_marker_if_exists(
747 'SCRIPT',
748 '<script',
749 '#<(?:no)?script.*?<\/(?:no)?script>#is',
750 $in
751 );
752
753 // extract img tags and add lazyload attribs.
754 if ( preg_match_all( '#<img[^>]*src[^>]*>#Usmi', $out, $matches ) ) {
755 foreach ( $matches[0] as $tag ) {
756 if ( $this->should_lazyload( $out ) ) {
757 $to_replace[ $tag ] = $this->add_lazyload( $tag );
758 }
759 }
760 $out = str_replace( array_keys( $to_replace ), array_values( $to_replace ), $out );
761 }
762
763 // and also lazyload picture tag.
764 $out = $this->process_picture_tag( $out, false, true );
765
766 // and inline style blocks with background-image.
767 $out = $this->process_bgimage( $out );
768
769 // restore noscript tags.
770 $out = autoptimizeBase::restore_marked_content(
771 'SCRIPT',
772 $out
773 );
774
775 return $out;
776 }
777
778 public function add_lazyload( $tag, $placeholder = '' ) {
779 // adds actual lazyload-attributes to an image node.
780 $this->lazyload_counter++;
781
782 $_lazyload_from_nth = '';
783 if ( array_key_exists( 'autoptimize_imgopt_number_field_7', $this->options ) ) {
784 $_lazyload_from_nth = $this->options['autoptimize_imgopt_number_field_7'];
785 }
786 $_lazyload_from_nth = apply_filters( 'autoptimize_filter_imgopt_lazyload_from_nth', $_lazyload_from_nth );
787
788 if ( str_ireplace( $this->get_lazyload_exclusions(), '', $tag ) === $tag && $this->lazyload_counter >= $_lazyload_from_nth ) {
789 $tag = $this->maybe_fix_missing_quotes( $tag );
790
791 // store original tag for use in noscript version.
792 $noscript_tag = '<noscript>' . autoptimizeUtils::remove_id_from_node( $tag ) . '</noscript>';
793
794 $lazyload_class = apply_filters( 'autoptimize_filter_imgopt_lazyload_class', 'lazyload' );
795
796 // insert lazyload class.
797 $tag = $this->inject_classes_in_tag( $tag, "$lazyload_class " );
798
799 if ( ! $placeholder || empty( $placeholder ) ) {
800 // get image width & heigth for placeholder fun (and to prevent content reflow).
801 $_get_size = $this->get_size_from_tag( $tag );
802 $width = $_get_size['width'];
803 $height = $_get_size['height'];
804 if ( false === $width || empty( $width ) ) {
805 $width = 210; // default width for SVG placeholder.
806 }
807 if ( false === $height || empty( $height ) ) {
808 $height = $width / 3 * 2; // if no height, base it on width using the 3/2 aspect ratio.
809 }
810
811 // insert the actual lazyload stuff.
812 // see https://css-tricks.com/preventing-content-reflow-from-lazy-loaded-images/ for great read on why we're using empty svg's.
813 $placeholder = apply_filters( 'autoptimize_filter_imgopt_lazyload_placeholder', $this->get_default_lazyload_placeholder( $width, $height ) );
814 }
815
816 $tag = preg_replace( '/(\s)src=/', ' src=\'' . $placeholder . '\' data-src=', $tag );
817 $tag = preg_replace( '/(\s)srcset=/', ' data-srcset=', $tag );
818
819 // move sizes to data-sizes unless filter says no.
820 if ( apply_filters( 'autoptimize_filter_imgopt_lazyload_move_sizes', true ) ) {
821 $tag = str_replace( ' sizes=', ' data-sizes=', $tag );
822 }
823
824 // add the noscript-tag from earlier.
825 $tag = $noscript_tag . $tag;
826 $tag = apply_filters( 'autoptimize_filter_imgopt_lazyloaded_img', $tag );
827 }
828
829 return $tag;
830 }
831
832 public function add_lazyload_js_footer() {
833 if ( false === autoptimizeMain::should_buffer() ) {
834 return;
835 }
836
837 // The JS will by default be excluded form autoptimization but this can be changed with a filter.
838 $noptimize_flag = '';
839 if ( apply_filters( 'autoptimize_filter_imgopt_lazyload_js_noptimize', true ) ) {
840 $noptimize_flag = ' data-noptimize="1"';
841 }
842
843 $lazysizes_js = plugins_url( 'external/js/lazysizes.min.js?ao_version=' . AUTOPTIMIZE_PLUGIN_VERSION, __FILE__ );
844 $cdn_url = $this->get_cdn_url();
845 if ( ! empty( $cdn_url ) ) {
846 $cdn_url = rtrim( $cdn_url, '/' );
847 $lazysizes_js = str_replace( AUTOPTIMIZE_WP_SITE_URL, $cdn_url, $lazysizes_js );
848 }
849
850 $type_js = '';
851 if ( apply_filters( 'autoptimize_filter_cssjs_addtype', false ) ) {
852 $type_js = ' type="text/javascript"';
853 }
854
855 // Adds lazyload CSS & JS to footer, using echo because wp_enqueue_script seems not to support pushing attributes (async).
856 echo apply_filters( 'autoptimize_filter_imgopt_lazyload_cssoutput', '<noscript><style>.lazyload{display:none;}</style></noscript>' );
857 echo apply_filters( 'autoptimize_filter_imgopt_lazyload_jsconfig', '<script' . $type_js . $noptimize_flag . '>window.lazySizesConfig=window.lazySizesConfig||{};window.lazySizesConfig.loadMode=1;</script>' );
858 echo apply_filters( 'autoptimize_filter_imgopt_lazyload_js', '<script async' . $type_js . $noptimize_flag . ' src=\'' . $lazysizes_js . '\'></script>' );
859
860 // And add webp detection and loading JS.
861 if ( $this->should_ngimg() ) {
862 // Add AVIF code, can be disabled for now to only do webp.
863 if ( apply_filters( 'autoptimize_filter_imgopt_do_avif', true ) ) {
864 $_ngimg_detect = 'function c_img(a,b){src="avif"==b?"data:image/avif;base64,AAAAIGZ0eXBhdmlmAAAAAGF2aWZtaWYxbWlhZk1BMUIAAADybWV0YQAAAAAAAAAoaGRscgAAAAAAAAAAcGljdAAAAAAAAAAAAAAAAGxpYmF2aWYAAAAADnBpdG0AAAAAAAEAAAAeaWxvYwAAAABEAAABAAEAAAABAAABGgAAABoAAAAoaWluZgAAAAAAAQAAABppbmZlAgAAAAABAABhdjAxQ29sb3IAAAAAamlwcnAAAABLaXBjbwAAABRpc3BlAAAAAAAAAAEAAAABAAAAEHBpeGkAAAAAAwgICAAAAAxhdjFDgQ0MAAAAABNjb2xybmNseAACAAIAAYAAAAAXaXBtYQAAAAAAAAABAAEEAQKDBAAAACJtZGF0EgAKCBgADsgQEAwgMgwf8AAAWAAAAACvJ+o=":"data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==";var c=new Image;c.onload=function(){var d=0<c.width&&0<c.height;a(d,b)},c.onerror=function(){a(!1,b)},c.src=src}function s_img(a,b){w=window,"avif"==b?!1==a?c_img(s_img,"webp"):w.ngImg="avif":!1==a?w.ngImg=!1:w.ngImg="webp"}c_img(s_img,"avif");';
865 $_ngimg_load = 'document.addEventListener("lazybeforeunveil",function({target:a}){window.ngImg&&["data-src","data-srcset"].forEach(function(b){attr=a.getAttribute(b),null!==attr&&-1==attr.indexOf("/client/to_")&&a.setAttribute(b,attr.replace(/\/client\//,"/client/to_"+window.ngImg+","))})});';
866 } else {
867 $_ngimg_detect = "function c_webp(A){var n=new Image;n.onload=function(){var e=0<n.width&&0<n.height;A(e)},n.onerror=function(){A(!1)},n.src='data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=='}function s_webp(e){window.supportsWebP=e}c_webp(s_webp);";
868 $_ngimg_load = "document.addEventListener('lazybeforeunveil',function({target:b}){window.supportsWebP&&['data-src','data-srcset'].forEach(function(c){attr=b.getAttribute(c),null!==attr&&-1==attr.indexOf('/client/to_webp')&&b.setAttribute(c,attr.replace(/\/client\//,'/client/to_webp,'))})});";
869 }
870 // Keeping autoptimize_filter_imgopt_webp_js filter for now, but it is deprecated as not only for webp any more.
871 $_ngimg_output = apply_filters( 'autoptimize_filter_imgopt_webp_js', '<script' . $type_js . $noptimize_flag . '>' . $_ngimg_detect . $_ngimg_load . '</script>' );
872 echo apply_filters( 'autoptimize_filter_imgopt_ngimg_js', $_ngimg_output );
873 }
874 }
875
876 public function get_cdn_url() {
877 // getting CDN url here to avoid having to make bigger changes to autoptimizeBase.
878 static $cdn_url = null;
879
880 if ( null === $cdn_url ) {
881 $cdn_url = autoptimizeOptionWrapper::get_option( 'autoptimize_cdn_url', '' );
882 $cdn_url = autoptimizeUtils::tweak_cdn_url_if_needed( $cdn_url );
883 $cdn_url = apply_filters( 'autoptimize_filter_base_cdnurl', $cdn_url );
884 }
885
886 return $cdn_url;
887 }
888
889 public function get_lazyload_exclusions() {
890 // returns array of strings that if found in an <img tag will stop the img from being lazy-loaded.
891 static $exclude_lazyload_array = null;
892
893 if ( null === $exclude_lazyload_array ) {
894 $options = $this->options;
895
896 // set default exclusions.
897 $exclude_lazyload_array = array( 'skip-lazy', 'data-no-lazy', 'notlazy', 'data-src', 'data-srcset', 'data:image/', 'data-lazyload', 'rev-slidebg', 'loading="eager"' );
898
899 // add from setting.
900 if ( array_key_exists( 'autoptimize_imgopt_text_field_5', $options ) ) {
901 $exclude_lazyload_option = $options['autoptimize_imgopt_text_field_5'];
902 if ( ! empty( $exclude_lazyload_option ) ) {
903 $exclude_lazyload_array = array_merge( $exclude_lazyload_array, array_filter( array_map( 'trim', explode( ',', $options['autoptimize_imgopt_text_field_5'] ) ) ) );
904 }
905 }
906
907 // and filter for developer-initiated changes.
908 $exclude_lazyload_array = apply_filters( 'autoptimize_filter_imgopt_lazyload_exclude_array', $exclude_lazyload_array );
909 }
910
911 return $exclude_lazyload_array;
912 }
913
914 public function inject_classes_in_tag( $tag, $target_class ) {
915 if ( strpos( $tag, 'class=' ) !== false ) {
916 $tag = preg_replace( '/(\sclass\s?=\s?("|\'))/', '$1' . $target_class, $tag );
917 } else {
918 $tag = preg_replace( '/(<[a-zA-Z]*)\s/', '$1 class="' . trim( $target_class ) . '" ', $tag );
919 }
920
921 return $tag;
922 }
923
924 public function get_default_lazyload_placeholder( $imgopt_w, $imgopt_h ) {
925 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';
926 }
927
928 public function should_ngimg() {
929 static $ngimg_return = null;
930
931 if ( is_null( $ngimg_return ) ) {
932 // webp only works if imgopt and lazyload are also active.
933 if ( ! empty( $this->options['autoptimize_imgopt_checkbox_field_4'] ) && ! empty( $this->options['autoptimize_imgopt_checkbox_field_3'] ) && $this->imgopt_active() ) {
934 $ngimg_return = true;
935 } else {
936 $ngimg_return = false;
937 }
938 }
939
940 return $ngimg_return;
941 }
942
943 public function process_picture_tag( $in, $imgopt = false, $lazy = false ) {
944 // check if "<picture" is present and if filter allows us to process <picture>.
945 if ( strpos( $in, '<picture' ) === false || apply_filters( 'autoptimize_filter_imgopt_dopicture', true ) === false ) {
946 return $in;
947 }
948
949 $_exclusions = $this->get_lazyload_exclusions();
950 $to_replace_pict = array();
951
952 // extract and process each picture-node.
953 preg_match_all( '#<picture.*</picture>#Usmi', $in, $_pictures, PREG_SET_ORDER );
954 foreach ( $_pictures as $_picture ) {
955 $_picture = $this->maybe_fix_missing_quotes( $_picture );
956 if ( strpos( $_picture[0], '<source ' ) !== false && preg_match_all( '#<source .*srcset=(?:"|\')(?!data)(.*)(?:"|\').*>#Usmi', $_picture[0], $_sources, PREG_SET_ORDER ) !== false ) {
957 foreach ( $_sources as $_source ) {
958 $_picture_replacement = $_source[0];
959
960 // should we optimize the image?
961 if ( $imgopt && $this->can_optimize_image( $_source[1], $_picture[0] ) ) {
962 $_picture_replacement = str_replace( $_source[1], $this->build_imgopt_url( $_source[1] ), $_picture_replacement );
963 }
964 // should we lazy-load?
965 if ( $lazy && $this->should_lazyload() && str_ireplace( $_exclusions, '', $_picture_replacement ) === $_picture_replacement ) {
966 $_picture_replacement = str_replace( ' srcset=', ' data-srcset=', $_picture_replacement );
967 }
968 $to_replace_pict[ $_source[0] ] = $_picture_replacement;
969 }
970 }
971 }
972
973 // and return the fully procesed $in.
974 $out = str_replace( array_keys( $to_replace_pict ), array_values( $to_replace_pict ), $in );
975
976 return $out;
977 }
978
979 public function process_bgimage( $in ) {
980 if ( strpos( $in, 'background-image:' ) !== false && apply_filters( 'autoptimize_filter_imgopt_lazyload_backgroundimages', true ) ) {
981 $out = preg_replace_callback(
982 '/(<(?:article|aside|body|div|footer|header|p|section|span|table)[^>]*)\sstyle=(?:"|\')[^<>]*?background-image:\s?url\((?:"|\')?([^"\')]*)(?:"|\')?\)[^>]*/',
983 array( $this, 'lazyload_bgimg_callback' ),
984 $in
985 );
986 return $out;
987 }
988 return $in;
989 }
990
991 public function lazyload_bgimg_callback( $matches ) {
992 if ( str_ireplace( $this->get_lazyload_exclusions(), '', $matches[0] ) === $matches[0] ) {
993 // get placeholder & lazyload class strings.
994 $placeholder = apply_filters( 'autoptimize_filter_imgopt_lazyload_placeholder', $this->get_default_lazyload_placeholder( 500, 300 ) );
995 $lazyload_class = apply_filters( 'autoptimize_filter_imgopt_lazyload_class', 'lazyload' );
996 // replace background-image URL with SVG placeholder.
997 $out = str_replace( 'url(' . $matches[2], 'url(' . $placeholder, $matches[0] );
998 // sanitize bgimg src for quote sillyness.
999 $bgimg_src = $this->fix_silly_bgimg_quotes( $matches[2] );
1000 // add data-bg attribute with real background-image URL for lazyload to pick up.
1001 $out = str_replace( $matches[1], $matches[1] . ' data-bg="' . $bgimg_src . '"', $out );
1002 // and finally add lazyload class to tag.
1003 $out = $this->inject_classes_in_tag( $out, "$lazyload_class " );
1004 return $out;
1005 }
1006 return $matches[0];
1007 }
1008
1009 public function fix_silly_bgimg_quotes( $tag_in ) {
1010 // some themes/ pagebuilders wrap backgroundimages in HTML-encoded quotes (or linebreaks) which breaks imgopt/ lazyloading, this removes them.
1011 return trim( str_replace( array( "\r\n", '&quot;', '&#034;', '&apos;', '&#039;' ), '', $tag_in ) );
1012 }
1013
1014 public function maybe_fix_missing_quotes( $tag_in ) {
1015 // 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.
1016 if ( file_exists( WP_PLUGIN_DIR . '/w3-total-cache/w3-total-cache.php' ) && class_exists( 'Minify_HTML' ) && apply_filters( 'autoptimize_filter_imgopt_fixquotes', true ) ) {
1017 $tag_out = preg_replace( '/class\s?=([^("|\')]*)(\s|>)/U', 'class=\'$1\'$2', $tag_in );
1018 $tag_out = preg_replace( '/\s(width|height)=(?:"|\')?([^\s"\'>]*)(?:"|\')?/', ' $1=\'$2\'', $tag_out );
1019 return $tag_out;
1020 } else {
1021 return $tag_in;
1022 }
1023 }
1024
1025 /**
1026 * Admin page logic and related functions below.
1027 */
1028 public function imgopt_admin_menu()
1029 {
1030 // no acces if multisite and not network admin and no site config allowed.
1031 if ( autoptimizeConfig::should_show_menu_tabs() ) {
1032 add_submenu_page(
1033 null,
1034 'autoptimize_imgopt',
1035 'autoptimize_imgopt',
1036 'manage_options',
1037 'autoptimize_imgopt',
1038 array( $this, 'imgopt_options_page' )
1039 );
1040 }
1041 register_setting( 'autoptimize_imgopt_settings', 'autoptimize_imgopt_settings' );
1042 }
1043
1044 public function add_imgopt_tab( $in )
1045 {
1046 if ( autoptimizeConfig::should_show_menu_tabs() ) {
1047 $in = array_merge( $in, array( 'autoptimize_imgopt' => __( 'Images', 'autoptimize' ) ) );
1048 }
1049
1050 return $in;
1051 }
1052
1053 public function imgopt_options_page()
1054 {
1055 // Check querystring for "refreshCacheChecker" and call cachechecker if so.
1056 if ( array_key_exists( 'refreshImgProvStats', $_GET ) && 1 == $_GET['refreshImgProvStats'] ) {
1057 $this->query_img_provider_stats( true );
1058 }
1059
1060 $options = $this->fetch_options();
1061 $sp_url_suffix = $this->get_service_url_suffix();
1062 ?>
1063 <style>
1064 #ao_settings_form {background: white;border: 1px solid #ccc;padding: 1px 15px;margin: 15px 10px 10px 0;}
1065 #ao_settings_form .form-table th {font-weight: normal;}
1066 #autoptimize_imgopt_descr{font-size: 120%;}
1067 </style>
1068 <script>document.title = "Autoptimize: <?php _e( 'Images', 'autoptimize' ); ?> " + document.title;</script>
1069 <div class="wrap">
1070 <h1><?php _e( 'Autoptimize Settings', 'autoptimize' ); ?></h1>
1071 <?php echo autoptimizeConfig::ao_admin_tabs(); ?>
1072 <?php if ( 'down' === $options['availabilities']['extra_imgopt']['status'] ) { ?>
1073 <div class="notice-warning notice"><p>
1074 <?php
1075 // translators: "Autoptimize support forum" will appear in a "a href".
1076 echo sprintf( __( '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>' );
1077 ?>
1078 </p></div>
1079 <?php } ?>
1080
1081 <?php if ( 'launch' === $options['availabilities']['extra_imgopt']['status'] && ! autoptimizeImages::instance()->launch_ok() ) { ?>
1082 <div class="notice-warning notice"><p>
1083 <?php _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' ); ?>
1084 </p></div>
1085 <?php } ?>
1086
1087 <?php if ( class_exists( 'Jetpack' ) && method_exists( 'Jetpack', 'get_active_modules' ) && in_array( 'photon', Jetpack::get_active_modules() ) ) { ?>
1088 <div class="notice-warning notice"><p>
1089 <?php
1090 // translators: "disable Jetpack's site accelerator for images" will appear in a "a href" linking to the jetpack settings page.
1091 echo sprintf( __( '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>' );
1092 ?>
1093 </p></div>
1094 <?php } ?>
1095 <form id='ao_settings_form' action='<?php echo admin_url( 'options.php' ); ?>' method='post'>
1096 <?php settings_fields( 'autoptimize_imgopt_settings' ); ?>
1097 <h2><?php _e( 'Image optimization', 'autoptimize' ); ?></h2>
1098 <span id='autoptimize_imgopt_descr'><?php _e( 'Make your site significantly faster by just ticking a couple of checkboxes to optimize and lazy load your images, WebP and AVIF support included!', 'autoptimize' ); ?></span>
1099 <table class="form-table">
1100 <tr>
1101 <th scope="row"><?php _e( 'Optimize Images', 'autoptimize' ); ?></th>
1102 <td>
1103 <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 _e( 'Optimize images on the fly and serve them from Shortpixel\'s global CDN.', 'autoptimize' ); ?></label>
1104 <?php
1105 // show shortpixel status.
1106 $_notice = autoptimizeImages::instance()->get_imgopt_status_notice();
1107 if ( $_notice ) {
1108 switch ( $_notice['status'] ) {
1109 case 2:
1110 $_notice_color = 'green';
1111 break;
1112 case 1:
1113 $_notice_color = 'orange';
1114 break;
1115 case -1:
1116 case -2:
1117 case -3:
1118 $_notice_color = 'red';
1119 break;
1120 default:
1121 $_notice_color = 'green';
1122 }
1123 echo apply_filters( 'autoptimize_filter_imgopt_settings_status', '<p><strong><span style="color:' . $_notice_color . ';">' . __( 'Shortpixel status: ', 'autoptimize' ) . '</span></strong>' . $_notice['notice'] . '</p>' );
1124 } else {
1125 // translators: link points to shortpixel.
1126 $upsell_msg_1 = '<p>' . sprintf( __( 'Get more Google love by speeding up your website. Start serving on-the-fly optimized images (also in the "next-gen" <strong>WebP</strong> and <strong>AVIF</strong> image formats) by %1$sShortPixel%2$s. The optimized images are cached and served from %3$sShortPixel\'s global CDN%2$s.', 'autoptimize' ), '<a href="https://shortpixel.com/aospai' . $sp_url_suffix . '" target="_blank">', '</a>', '<a href="https://help.shortpixel.com/article/62-where-does-the-cdn-has-pops" target="_blank">' );
1127 if ( 'launch' === $options['availabilities']['extra_imgopt']['status'] ) {
1128 $upsell_msg_2 = __( 'For a limited time only, this service is offered free for all Autoptimize users, <b>don\'t miss the chance to test it</b> and see how much it could improve your site\'s speed.', 'autoptimize' );
1129 } else {
1130 // translators: link points to shortpixel.
1131 $upsell_msg_2 = sprintf( __( '%1$sSign-up now%2$s to receive x2 more CDN traffic or image optimization credits for free! This offer also applies to any future plan that you\'ll choose to purchase.', 'autoptimize' ), '<a href="https://shortpixel.com/aospai' . $sp_url_suffix . '" target="_blank">', '</a>' );
1132 }
1133 echo apply_filters( 'autoptimize_imgopt_imgopt_settings_copy', $upsell_msg_1 . ' ' . $upsell_msg_2 . '</p>' );
1134 }
1135 // translators: link points to shortpixel FAQ.
1136 $faqcopy = sprintf( __( '<strong>Questions</strong>? Have 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>' );
1137 $faqcopy = $faqcopy . ' ' . __( 'Only works for websites and images that are publicly available.', 'autoptimize' );
1138 // translators: links points to shortpixel TOS & Privacy Policy.
1139 $toscopy = sprintf( __( '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/pp' . $sp_url_suffix . '" target="_blank">', '</a>' );
1140 echo apply_filters( 'autoptimize_imgopt_imgopt_settings_tos', '<p>' . $faqcopy . ' ' . $toscopy . '</p>' );
1141 ?>
1142 </td>
1143 </tr>
1144 <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"'; } ?>>
1145 <th scope="row"><?php _e( 'Optimization exclusions', 'autoptimize' ); ?></th>
1146 <td>
1147 <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 _e( 'Comma-separated list of image classes or filenames that should not be optimized.', 'autoptimize' ); ?></label>
1148 </td>
1149 </tr>
1150 <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"'; } ?>>
1151 <th scope="row"><?php _e( 'Image Optimization quality', 'autoptimize' ); ?></th>
1152 <td>
1153 <label>
1154 <select name='autoptimize_imgopt_settings[autoptimize_imgopt_select_field_2]'>
1155 <?php
1156 $_imgopt_array = autoptimizeImages::instance()->get_img_quality_array();
1157 $_imgopt_val = autoptimizeImages::instance()->get_img_quality_setting();
1158
1159 foreach ( $_imgopt_array as $key => $value ) {
1160 echo '<option value="' . $key . '"';
1161 if ( $_imgopt_val == $key ) {
1162 echo ' selected';
1163 }
1164 echo '>' . ucfirst( $value ) . '</option>';
1165 }
1166 echo "\n";
1167 ?>
1168 </select>
1169 </label>
1170 <p>
1171 <?php
1172 // translators: link points to shortpixel image test page.
1173 echo apply_filters( 'autoptimize_imgopt_imgopt_quality_copy', sprintf( __( 'You can %1$stest compression levels here%2$s.', 'autoptimize' ), '<a href="https://shortpixel.com/oic' . $sp_url_suffix . '" target="_blank">', '</a>' ) );
1174 ?>
1175 </p>
1176 </td>
1177 </tr>
1178 <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"'; } ?>>
1179 <th scope="row"><?php _e( 'Load WebP or AVIF in supported browsers?', 'autoptimize' ); ?></th>
1180 <td>
1181 <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_3'] ) { echo 'checked="checked"'; } ?> value='1'><?php _e( 'Automatically serve "next-gen" WebP or AVIF image formats to any browser that supports it (requires lazy load to be active).', 'autoptimize' ); ?></label>
1182 </td>
1183 </tr>
1184 <tr>
1185 <th scope="row"><?php _e( 'Lazy-load images?', 'autoptimize' ); ?></th>
1186 <td>
1187 <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 _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>
1188 </td>
1189 </tr>
1190 <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"'; } ?>>
1191 <th scope="row"><?php _e( 'Lazy-load exclusions', 'autoptimize' ); ?></th>
1192 <td>
1193 <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 _e( 'Comma-separated list of to be excluded image classes or filenames.', 'autoptimize' ); ?></label>
1194 </td>
1195 </tr>
1196 <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"'; } ?>>
1197 <th scope="row"><?php _e( 'Lazy-load from nth image', 'autoptimize' ); ?></th>
1198 <td>
1199 <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 '0'; } ?>'><br /><?php _e( 'Don\'t lazyload the first X images, \'0\' lazyloads all.', 'autoptimize' ); ?></label>
1200 </td>
1201 </tr>
1202 </table>
1203 <p class="submit"><input type="submit" name="submit" id="submit" class="button button-primary" value="<?php _e( 'Save Changes', 'autoptimize' ); ?>" /></p>
1204 </form>
1205 <script>
1206 jQuery(document).ready(function() {
1207 jQuery("#autoptimize_imgopt_checkbox").change(function() {
1208 if (this.checked) {
1209 jQuery("#autoptimize_imgopt_quality").show("slow");
1210 jQuery("#autoptimize_imgopt_ngimg").show("slow");
1211 jQuery("#autoptimize_imgopt_optimization_exclusions").show("slow");
1212 } else {
1213 jQuery("#autoptimize_imgopt_quality").hide("slow");
1214 jQuery("#autoptimize_imgopt_ngimg").hide("slow");
1215 jQuery("#autoptimize_imgopt_optimization_exclusions").hide("slow");
1216 }
1217 });
1218 jQuery("#autoptimize_imgopt_ngimg_checkbox").change(function() {
1219 if (this.checked) {
1220 jQuery("#autoptimize_imgopt_lazyload_checkbox")[0].checked = true;
1221 jQuery(".autoptimize_lazyload_child").show("slow");
1222 }
1223 });
1224 jQuery("#autoptimize_imgopt_lazyload_checkbox").change(function() {
1225 if (this.checked) {
1226 jQuery(".autoptimize_lazyload_child").show("slow");
1227 } else {
1228 jQuery(".autoptimize_lazyload_child").hide("slow");
1229 jQuery("#autoptimize_imgopt_ngimg_checkbox")[0].checked = false;
1230 }
1231 });
1232 });
1233 </script>
1234 <?php
1235 }
1236
1237 /**
1238 * Ïmg opt status as used on dashboard.
1239 */
1240 public function get_imgopt_status_notice() {
1241 if ( $this->imgopt_active() ) {
1242 $_imgopt_notice = '';
1243 $_stat = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_provider_stat', '' );
1244 $_site_host = AUTOPTIMIZE_SITE_DOMAIN;
1245 $_imgopt_upsell = 'https://shortpixel.com/aospai/af/SPZURYE109483/' . $_site_host;
1246 $_imgopt_assoc = 'https://shortpixel.helpscoutdocs.com/article/94-how-to-associate-a-domain-to-my-account';
1247 $_imgopt_unreach = 'https://shortpixel.helpscoutdocs.com/article/148-why-are-my-images-redirected-from-cdn-shortpixel-ai';
1248
1249 if ( is_array( $_stat ) ) {
1250 if ( 1 == $_stat['Status'] ) {
1251 // translators: "add more credits" will appear in a "a href".
1252 $_imgopt_notice = sprintf( __( '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.', 'autoptimize' ), '<a href="' . $_imgopt_upsell . '" target="_blank">', '</a>' );
1253 } elseif ( -1 == $_stat['Status'] || -2 == $_stat['Status'] ) {
1254 // translators: "add more credits" will appear in a "a href".
1255 $_imgopt_notice = sprintf( __( 'Your ShortPixel image optimization and CDN quota was used, %1$sadd more credits%2$s to keep fast serving optimized images on your site', 'autoptimize' ), '<a href="' . $_imgopt_upsell . '" target="_blank">', '</a>' );
1256 // translators: "associate your domain" will appear in a "a href".
1257 $_imgopt_notice = $_imgopt_notice . ' ' . sprintf( __( '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>' );
1258 } elseif ( -3 == $_stat['Status'] ) {
1259 // translators: "check the documentation here" will appear in a "a href".
1260 $_imgopt_notice = sprintf( __( '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>' );
1261 } else {
1262 $_imgopt_upsell = 'https://shortpixel.com/g/af/SPZURYE109483';
1263 // translators: "log in to check your account" will appear in a "a href".
1264 $_imgopt_notice = sprintf( __( '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>' );
1265 }
1266
1267 // add info on freshness + refresh link if status is not 2 (good shape).
1268 if ( 2 != $_stat['Status'] ) {
1269 $_imgopt_stats_refresh_url = add_query_arg( array(
1270 'page' => 'autoptimize_imgopt',
1271 'refreshImgProvStats' => '1',
1272 ), admin_url( 'options-general.php' ) );
1273 if ( $_stat && array_key_exists( 'timestamp', $_stat ) && ! empty( $_stat['timestamp'] ) ) {
1274 $_imgopt_stats_last_run = __( 'based on status at ', 'autoptimize' ) . date_i18n( autoptimizeOptionWrapper::get_option( 'time_format' ), $_stat['timestamp'] );
1275 } else {
1276 $_imgopt_stats_last_run = __( 'based on previously fetched data', 'autoptimize' );
1277 }
1278 $_imgopt_notice .= ' (' . $_imgopt_stats_last_run . ', ';
1279 // translators: "here to refresh" links to the Autoptimize Extra page and forces a refresh of the img opt stats.
1280 $_imgopt_notice .= sprintf( __( 'you can click %1$shere to refresh your quota status%2$s', 'autoptimize' ), '<a href="' . $_imgopt_stats_refresh_url . '">', '</a>).' );
1281 }
1282
1283 // and make the full notice filterable.
1284 $_imgopt_notice = apply_filters( 'autoptimize_filter_imgopt_notice', $_imgopt_notice );
1285
1286 return array(
1287 'status' => $_stat['Status'],
1288 'notice' => $_imgopt_notice,
1289 );
1290 }
1291 }
1292 return false;
1293 }
1294
1295 public static function get_imgopt_status_notice_wrapper() {
1296 // needed for notice being shown in autoptimizeCacheChecker.php.
1297 $self = new self();
1298 return $self->get_imgopt_status_notice();
1299 }
1300
1301 /**
1302 * Get img provider stats (used to display notice).
1303 */
1304 public function query_img_provider_stats( $_refresh = false ) {
1305 if ( ! empty( $this->options['autoptimize_imgopt_checkbox_field_1'] ) ) {
1306 $url = '';
1307 $stat_dom = 'https://no-cdn.shortpixel.ai/';
1308 $endpoint = $stat_dom . 'read-domain/';
1309 $domain = AUTOPTIMIZE_SITE_DOMAIN;
1310
1311 // make sure parse_url result makes sense, keeping $url empty if not.
1312 if ( $domain && ! empty( $domain ) ) {
1313 $url = $endpoint . $domain;
1314 if ( true === $_refresh ) {
1315 $url = $url . '/refresh';
1316 }
1317 }
1318
1319 $url = apply_filters(
1320 'autoptimize_filter_imgopt_stat_url',
1321 $url
1322 );
1323
1324 // only do the remote call if $url is not empty to make sure no parse_url
1325 // weirdness results in useless calls.
1326 if ( ! empty( $url ) ) {
1327 $response = wp_remote_get( $url );
1328 if ( ! is_wp_error( $response ) ) {
1329 if ( '200' == wp_remote_retrieve_response_code( $response ) ) {
1330 $stats = json_decode( wp_remote_retrieve_body( $response ), true );
1331 autoptimizeOptionWrapper::update_option( 'autoptimize_imgopt_provider_stat', $stats );
1332 }
1333 }
1334 }
1335 }
1336 }
1337
1338 public static function get_img_provider_stats()
1339 {
1340 // wrapper around query_img_provider_stats() so we can get to $this->options from cronjob() in autoptimizeCacheChecker.
1341 $self = new self();
1342 return $self->query_img_provider_stats();
1343 }
1344
1345 /**
1346 * Determines and returns the service launch status.
1347 *
1348 * @return bool
1349 */
1350 public function launch_ok()
1351 {
1352 static $launch_status = null;
1353
1354 if ( null === $launch_status ) {
1355 $avail_imgopt = $this->options['availabilities']['extra_imgopt'];
1356 $magic_number = intval( substr( md5( parse_url( AUTOPTIMIZE_WP_SITE_URL, PHP_URL_HOST ) ), 0, 3 ), 16 );
1357 $has_launched = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_launched', '' );
1358 $launch_status = false;
1359 if ( $has_launched || ( is_array( $avail_imgopt ) && array_key_exists( 'launch-threshold', $avail_imgopt ) && $magic_number < $avail_imgopt['launch-threshold'] ) ) {
1360 $launch_status = true;
1361 if ( ! $has_launched ) {
1362 autoptimizeOptionWrapper::update_option( 'autoptimize_imgopt_launched', 'on' );
1363 }
1364 }
1365 }
1366
1367 return $launch_status;
1368 }
1369
1370 public static function launch_ok_wrapper() {
1371 // needed for "plug" notice in autoptimizeMain.php.
1372 $self = new self();
1373 return $self->launch_ok();
1374 }
1375
1376 public function get_imgopt_provider_userstatus() {
1377 static $_provider_userstatus = null;
1378
1379 if ( is_null( $_provider_userstatus ) ) {
1380 $_stat = autoptimizeOptionWrapper::get_option( 'autoptimize_imgopt_provider_stat', '' );
1381 if ( is_array( $_stat ) ) {
1382 if ( array_key_exists( 'Status', $_stat ) ) {
1383 $_provider_userstatus['Status'] = $_stat['Status'];
1384 } else {
1385 // if no stats then we assume all is well.
1386 $_provider_userstatus['Status'] = 2;
1387 }
1388 if ( array_key_exists( 'timestamp', $_stat ) ) {
1389 $_provider_userstatus['timestamp'] = $_stat['timestamp'];
1390 } else {
1391 // if no timestamp then we return "".
1392 $_provider_userstatus['timestamp'] = '';
1393 }
1394 } else {
1395 // no provider_stat yet, assume/ return all OK.
1396 $_provider_userstatus['Status'] = 2;
1397 $_provider_userstatus['timestamp'] = '';
1398 }
1399 }
1400
1401 return $_provider_userstatus;
1402 }
1403 }
1404