PluginProbe
Autoptimize / 3.0.3
Autoptimize v3.0.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 3.0.3, at classes/autoptimizeImages.php

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