PluginProbe
Autoptimize / 2.7.8
Autoptimize v2.7.8
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.7.8, at classes/autoptimizeImages.php

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