PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 3.1.1
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v3.1.1
4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 3.12.0 3.12.1 All 134 releases
optimole-wp / inc / manager.php

manager.php in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization 3.1.1, at inc/manager.php

638 lines 17.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class Optml_Manager. Adds hooks for processing tags and urls.
5 *
6 * @package \Optml\Inc
7 * @author Optimole <friends@optimole.com>
8 */
9 final class Optml_Manager {
10 /**
11 * Holds allowed compatibilities objects.
12 *
13 * @var Optml_compatibility[] Compatibilities objects.
14 */
15 public static $loaded_compatibilities = [];
16 /**
17 * Cached object instance.
18 *
19 * @var Optml_Manager
20 */
21 protected static $instance = null;
22 /**
23 * Holds the url replacer class.
24 *
25 * @access public
26 * @since 1.0.0
27 * @var Optml_Url_Replacer Replacer instance.
28 */
29 public $url_replacer;
30 /**
31 * Holds the tag replacer class.
32 *
33 * @access public
34 * @since 1.0.0
35 * @var Optml_Tag_Replacer Replacer instance.
36 */
37 public $tag_replacer;
38 /**
39 * Holds the lazyload replacer class.
40 *
41 * @access public
42 * @since 1.0.0
43 * @var Optml_Lazyload_Replacer Replacer instance.
44 */
45 public $lazyload_replacer;
46 /**
47 * Holds plugin settings.
48 *
49 * @var Optml_Settings WordPress settings.
50 */
51 protected $settings;
52 /**
53 * Possible integrations with different plugins.
54 *
55 * @var array Integrations classes.
56 */
57 private $possible_compatibilities = [
58 'shortcode_ultimate',
59 'foogallery',
60 'envira',
61 'beaver_builder',
62 'jet_elements',
63 'revslider',
64 'metaslider',
65 'essential_grid',
66 'yith_quick_view',
67 'cache_enabler',
68 'elementor_builder',
69 'divi_builder',
70 'thrive',
71 'master_slider',
72 'pinterest',
73 'sg_optimizer',
74 'wp_fastest_cache',
75 'swift_performance',
76 'w3_total_cache',
77 'translate_press',
78 'give_wp',
79 'smart_search_woocommerce',
80 ];
81 /**
82 * The current state of the buffer.
83 *
84 * @var boolean Buffer state.
85 */
86 private static $ob_started = false;
87
88 /**
89 * Class instance method.
90 *
91 * @codeCoverageIgnore
92 * @static
93 * @return Optml_Manager
94 * @since 1.0.0
95 * @access public
96 */
97 public static function instance() {
98 if ( null === self::$instance ) {
99 self::$instance = new self();
100 self::$instance->url_replacer = Optml_Url_Replacer::instance();
101 self::$instance->tag_replacer = Optml_Tag_Replacer::instance();
102 self::$instance->lazyload_replacer = Optml_Lazyload_Replacer::instance();
103
104 add_action( 'after_setup_theme', [ self::$instance, 'init' ] );
105 }
106
107 return self::$instance;
108 }
109
110 /**
111 * The initialize method.
112 */
113 public function init() {
114
115 $this->settings = new Optml_Settings();
116
117 foreach ( $this->possible_compatibilities as $compatibility_class ) {
118 $compatibility_class = 'Optml_' . $compatibility_class;
119 $compatibility = new $compatibility_class;
120
121 /**
122 * Check if we should load compatibility.
123 *
124 * @var Optml_compatibility $compatibility Class to register.
125 */
126 if ( $compatibility->should_load() ) {
127 if ( $compatibility->should_load_early() ) {
128 $compatibility->register();
129 continue;
130 }
131 self::$loaded_compatibilities[ $compatibility_class ] = $compatibility;
132 }
133 }
134
135 if ( ! $this->should_replace() ) {
136 return;
137 }
138 $this->register_hooks();
139 }
140
141 /**
142 * Check if we should rewrite the urls.
143 *
144 * @return bool If we can replace the image.
145 */
146 public function should_replace() {
147
148 if ( apply_filters( 'optml_should_replace_page', false ) ) {
149 return false;
150 }
151
152 if ( ( is_admin() && ! self::is_ajax_request() ) || ! $this->settings->is_connected() || ! $this->settings->is_enabled() || is_customize_preview() ) {
153 return false; // @codeCoverageIgnore
154 }
155 if ( array_key_exists( 'preview', $_GET ) && ! empty( $_GET['preview'] ) ) {
156 return false; // @codeCoverageIgnore
157 }
158
159 if ( array_key_exists( 'optml_off', $_GET ) && 'true' == $_GET['optml_off'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
160 return false; // @codeCoverageIgnore
161 }
162 if ( array_key_exists( 'elementor-preview', $_GET ) && ! empty( $_GET['elementor-preview'] ) ) {
163 return false; // @codeCoverageIgnore
164 }
165 if ( array_key_exists( 'ct_builder', $_GET ) && ! empty( $_GET['ct_builder'] ) ) {
166 return false; // @codeCoverageIgnore
167 }
168 if ( array_key_exists( 'et_fb', $_GET ) && ! empty( $_GET['et_fb'] ) ) {
169 return false; // @codeCoverageIgnore
170 }
171 if ( array_key_exists( 'tve', $_GET ) && $_GET['tve'] == 'true' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
172 return false; // @codeCoverageIgnore
173 }
174 if ( array_key_exists( 'trp-edit-translation', $_GET ) && ( $_GET['trp-edit-translation'] == 'true' || $_GET['trp-edit-translation'] == 'preview' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
175 return false; // @codeCoverageIgnore
176 }
177 if ( array_key_exists( 'context', $_GET ) && $_GET['context'] == 'edit' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
178 return false; // @codeCoverageIgnore
179 }
180 if ( array_key_exists( 'fb-edit', $_GET ) && ! empty( $_GET['fb-edit'] ) ) {
181 return false; // @codeCoverageIgnore
182 }
183 /**
184 * Disable replacement on POST request and when user is logged in, but allows for sample image call widget in dashboard
185 */
186 if (
187 isset( $_SERVER['REQUEST_METHOD'] ) &&
188 $_SERVER['REQUEST_METHOD'] == 'POST' && // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
189 is_user_logged_in()
190 && ( ! isset( $_GET['quality'] ) || ! current_user_can( 'manage_options' ) )
191 ) {
192 return false; // @codeCoverageIgnore
193 }
194 if ( class_exists( 'FLBuilderModel', false ) ) {
195 $post_data = FLBuilderModel::get_post_data();
196 if ( isset( $_GET['fl_builder'] ) || isset( $post_data['fl_builder'] ) ) {
197 return false;
198 }
199 }
200
201 return Optml_Filters::should_do_page( $this->settings->get_filters()[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_URL ] );
202 }
203
204 /**
205 * Check if we are in a ajax contex where we should enable replacement.
206 *
207 * @return bool Is ajax request?
208 */
209 public static function is_ajax_request() {
210 if ( apply_filters( 'optml_force_replacement_on', false ) === true ) {
211
212 return true;
213 }
214 if ( ! function_exists( 'is_user_logged_in' ) ) {
215 return false;
216 }
217 // Disable for logged in users to avoid unexpected results.
218 if ( is_user_logged_in() ) {
219 return false;
220 }
221
222 if ( ! function_exists( 'wp_doing_ajax' ) ) {
223 return false;
224 }
225 if ( ! wp_doing_ajax() ) {
226 return false;
227 }
228 if ( isset( $_REQUEST['action'] ) && strpos( $_REQUEST['action'], 'wpmdb' ) !== false ) {
229 return false;
230 }
231
232 return true;
233 }
234
235 /**
236 * Register frontend replacer hooks.
237 */
238 public function register_hooks() {
239
240 do_action( 'optml_replacer_setup' );
241 if ( $this->settings->get( 'native_lazyload' ) === 'disabled' ) {
242 add_filter( 'wp_lazy_loading_enabled', '__return_false' );
243 }
244 add_filter( 'the_content', [ $this, 'process_images_from_content' ], PHP_INT_MAX );
245 /**
246 * When we have to process cdn images, i.e MIRROR is defined,
247 * we need this as late as possible for other replacers to occur.
248 * Otherwise, we can hook first to avoid any other plugins to take care of replacement.
249 */
250 add_action(
251 self::is_ajax_request() ? 'init' : 'template_redirect',
252 [
253 $this,
254 'process_template_redirect_content',
255 ],
256 defined( 'OPTML_SITE_MIRROR' ) ? PHP_INT_MAX : PHP_INT_MIN
257 );
258 add_action( 'template_redirect', [ $this, 'register_after_setup' ] );
259 add_action( 'rest_api_init', [ $this, 'process_template_redirect_content' ], PHP_INT_MIN );
260
261 add_action( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX, 4 );
262
263 foreach ( self::$loaded_compatibilities as $registered_compatibility ) {
264 $registered_compatibility->register();
265 }
266 }
267
268 /**
269 * Run after Optimole is fully setup.
270 */
271 public function register_after_setup() {
272 do_action( 'optml_after_setup' );
273 }
274
275 /**
276 * Replace urls in post meta values.
277 *
278 * @param mixed $metadata Metadata.
279 * @param int $object_id Post id.
280 * @param string $meta_key Meta key.
281 * @param bool $single Is single.
282 *
283 * @return mixed Altered meta.
284 */
285 public function replace_meta( $metadata, $object_id, $meta_key, $single ) {
286
287 $meta_needed = '_elementor_data';
288
289 if ( isset( $meta_key ) && $meta_needed === $meta_key ) {
290 remove_filter( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX );
291
292 $current_meta = get_post_meta( $object_id, $meta_needed, $single );
293 add_filter( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX, 4 );
294
295 if ( ! is_string( $current_meta ) ) {
296 return $metadata;
297 }
298
299 return $this->replace_content( $current_meta );
300 }
301
302 // Return original if the check does not pass
303 return $metadata;
304 }
305
306 /**
307 * Filter raw HTML content for urls.
308 *
309 * @param string $html HTML to filter.
310 *
311 * @return mixed Filtered content.
312 */
313 public function replace_content( $html ) {
314
315 if ( defined( 'REST_REQUEST' ) && REST_REQUEST && is_user_logged_in() ) {
316 return $html;
317 }
318
319 $html = $this->add_html_class( $html );
320
321 $html = $this->process_images_from_content( $html );
322
323 if ( $this->settings->get( 'video_lazyload' ) === 'enabled' ) {
324 $html = apply_filters( 'optml_video_replace', $html );
325 if ( Optml_Lazyload_Replacer::found_iframe() === true ) {
326 if ( strpos( $html, Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT ) !== false ) {
327 $html = str_replace( Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT, Optml_Lazyload_Replacer::IFRAME_PLACEHOLDER_CLASS, $html );
328 } else {
329 $html = preg_replace( '/<head>(.*)<\/head>/ism', '<head> $1' . Optml_Lazyload_Replacer::IFRAME_PLACEHOLDER_STYLE . '</head>', $html );
330 }
331 }
332 }
333 $html = apply_filters( 'optml_url_pre_process', $html );
334
335 $html = $this->process_urls_from_content( $html );
336
337 $html = apply_filters( 'optml_url_post_process', $html );
338
339 return $html;
340 }
341
342 /**
343 * Adds a filter that allows adding classes to the HTML tag.
344 *
345 * @param string $content The HTML content.
346 *
347 * @return mixed
348 */
349 public function add_html_class( $content ) {
350 if ( empty( $content ) ) {
351 return $content;
352 }
353
354 $additional_html_classes = apply_filters( 'optml_additional_html_classes', [] );
355
356 if ( ! $additional_html_classes ) {
357 return $content;
358 }
359
360 if ( preg_match( '/<html.*>/ismU', $content, $matches, PREG_OFFSET_CAPTURE ) === 1 ) {
361
362 $add_classes = implode( ' ', $additional_html_classes );
363 foreach ( $matches as $match ) {
364 if ( strpos( $match[0], 'class' ) !== false ) {
365 $new_tag = str_replace( [ 'class="', "class='" ], [ 'class="' . $add_classes, "class='" . $add_classes ], $match[0] );
366 } else {
367 $new_tag = str_replace( 'html ', 'html class="' . $add_classes . '" ', $match[0] );
368 }
369
370 $content = str_replace( $match[0], $new_tag, $content );
371 }
372 }
373
374 return $content;
375 }
376
377 /**
378 * Adds a filter with detected images tags and the content.
379 *
380 * @param string $content The HTML content.
381 *
382 * @return mixed
383 */
384 public function process_images_from_content( $content ) {
385 if ( self::should_ignore_image_tags() ) {
386 return $content;
387 }
388 $images = self::parse_images_from_html( $content );
389 if ( empty( $images ) ) {
390 return $content;
391 }
392
393 return apply_filters( 'optml_content_images_tags', $content, $images );
394 }
395
396 /**
397 * Check if we are on a amp endpoint.
398 *
399 * IMPORTANT: This needs to be used after parse_query hook, otherwise will return false positives.
400 *
401 * @return bool
402 */
403 public static function should_ignore_image_tags() {
404 // Ignore image tags replacement in amp context as they are not available.
405 if ( function_exists( 'is_amp_endpoint' ) ) {
406 return is_amp_endpoint();
407 }
408 if ( function_exists( 'ampforwp_is_amp_endpoint' ) ) {
409 return ampforwp_is_amp_endpoint();
410 }
411
412 // Ignore image tag replacement in feed context as we don't need it.
413 if ( is_feed() ) {
414 return true;
415 }
416
417 return apply_filters( 'optml_should_ignore_image_tags', false ) === true;
418 }
419
420 /**
421 * Match all images and any relevant <a> tags in a block of HTML.
422 *
423 * @param string $content Some HTML.
424 *
425 * @return array An array of $images matches, where $images[0] is
426 * an array of full matches, and the link_url, img_tag,
427 * and img_url keys are arrays of those matches.
428 */
429 public static function parse_images_from_html( $content ) {
430 $images = [];
431
432 $header_start = null;
433 $header_end = null;
434
435 if ( preg_match( '/<header.*<\/header>/ismU', $content, $matches, PREG_OFFSET_CAPTURE ) === 1 ) {
436 $header_start = $matches[0][1];
437 $header_end = $header_start + strlen( $matches[0][0] );
438 }
439 $regex = '/(?:<a[^>]+?href=["|\'](?P<link_url>[^\s]+?)["|\'][^>]*?>\s*)?(?P<img_tag>(?:<noscript\s*>\s*)?<img[^>]*?\s?(?:' . implode( '|', array_merge( [ 'src' ], Optml_Tag_Replacer::possible_src_attributes() ) ) . ')=["\'\\\\]*?(?P<img_url>[' . Optml_Config::$chars . ']{10,}).*?>(?:\s*<\/noscript\s*>)?){1}(?:\s*<\/a>)?/ismu';
440
441 if ( preg_match_all( $regex, $content, $images, PREG_OFFSET_CAPTURE ) ) {
442 if ( OPTML_DEBUG ) {
443 do_action( 'optml_log', $images );
444 }
445 foreach ( $images as $key => $unused ) {
446 // Simplify the output as much as possible, mostly for confirming test results.
447 if ( is_numeric( $key ) && $key > 0 ) {
448 unset( $images[ $key ] );
449 continue;
450 }
451
452 $is_no_script = false;
453 foreach ( $unused as $url_key => $url_value ) {
454 if ( $key === 'img_url' ) {
455 $images[ $key ][ $url_key ] = rtrim( $url_value[0], '\\' );
456 continue;
457 }
458 $images[ $key ][ $url_key ] = $url_value[0];
459
460 if ( $key === 0 ) {
461 $images['in_header'][ $url_key ] = $header_start !== null ? ( $url_value[1] > $header_start && $url_value[1] < $header_end ) : false;
462
463 // Check if we are in the noscript context.
464 if ( $is_no_script === false ) {
465 $is_no_script = strpos( $images[0][ $url_key ], '<noscript' ) !== false ? true : false;
466 }
467 if ( $is_no_script ) {
468 $images['in_header'][ $url_key ] = true;
469 $is_no_script = strpos( $images[0][ $url_key ], '</noscript' ) !== false ? false : true;
470 }
471 }
472 }
473 }
474
475 return $images;
476 }
477
478 return [];
479 }
480
481 /**
482 * Process url replacement from raw html strings.
483 *
484 * @param string $html Raw html.
485 *
486 * @return string Processed string.
487 */
488 public function process_urls_from_content( $html ) {
489 $extracted_urls = $this->extract_urls_from_content( $html );
490 if ( OPTML_DEBUG ) {
491 do_action( 'optml_log', 'matched urls' );
492 do_action( 'optml_log', $extracted_urls );
493 }
494 return $this->do_url_replacement( $html, $extracted_urls );
495
496 }
497
498 /**
499 * Method to extract assets from content.
500 *
501 * @param string $content The HTML content.
502 *
503 * @return array
504 */
505 public function extract_urls_from_content( $content ) {
506 $extensions = array_keys( Optml_Config::$image_extensions );
507 if ( $this->settings->use_cdn() && ! self::should_ignore_image_tags() ) {
508 $extensions = array_merge( $extensions, array_keys( Optml_Config::$assets_extensions ) );
509 }
510 $regex = '/(?:[(|\s\';",=\]])((?:http|\/|\\\\){1}(?:[' . Optml_Config::$chars . ']{10,}\.(?:' . implode( '|', $extensions ) . ')))(?=(?:http|>|%3F|\?|"|&|,|\s|\'|\)|\||\\\\|}|\[))/Uu';
511 preg_match_all(
512 $regex,
513 $content,
514 $urls
515 );
516
517 return $this->normalize_urls( $urls[1] );
518 }
519
520 /**
521 * Normalize extracted urls.
522 *
523 * @param array $urls Raw urls extracted.
524 *
525 * @return array Normalized array.
526 */
527 private function normalize_urls( $urls ) {
528
529 $urls = array_map(
530 function ( $value ) {
531 $value = str_replace( '&quot;', '', $value );
532
533 return rtrim( $value, '\\";\'' );
534 },
535 $urls
536 );
537 $urls = array_unique( $urls );
538
539 return array_values( $urls );
540 }
541
542 /**
543 * Process string content and replace possible urls.
544 *
545 * @param string $html String content.
546 * @param array $extracted_urls Urls to check.
547 *
548 * @return string Processed html.
549 */
550 private function do_url_replacement( $html, $extracted_urls ) {
551 $extracted_urls = apply_filters( 'optml_extracted_urls', $extracted_urls );
552
553 if ( empty( $extracted_urls ) ) {
554 return $html;
555 }
556 $slashed_config = addcslashes( Optml_Config::$service_url, '/' );
557
558 $extracted_urls = array_filter(
559 $extracted_urls,
560 function ( $value ) use ( $slashed_config ) {
561 return strpos( $value, Optml_Config::$service_url ) === false && strpos( $value, $slashed_config ) === false;
562 }
563 );
564 $upload_resource = $this->tag_replacer->get_upload_resource();
565 $urls = array_combine( $extracted_urls, $extracted_urls );
566
567 $urls = array_map(
568 function ( $url ) use ( $upload_resource ) {
569 $is_slashed = strpos( $url, '\/' ) !== false;
570 $is_relative = strpos(
571 $url,
572 $is_slashed ?
573 addcslashes( $upload_resource['content_path'], '/' ) :
574 $upload_resource['content_path']
575 ) === 0;
576 if ( $is_relative ) {
577 $url = $upload_resource['content_host'] . $url;
578 }
579
580 return apply_filters( 'optml_content_url', $url );
581 },
582 $urls
583 );
584
585 foreach ( $urls as $origin => $replace ) {
586 $html = preg_replace( '/(?<![\/|:|\\w])' . preg_quote( $origin, '/' ) . '/m', $replace, $html );
587 }
588
589 return $html;
590 }
591
592 /**
593 * Init html replacer handler.
594 */
595 public function process_template_redirect_content() {
596 // Early exit if function was already called, we don't want duplicate ob_start
597 if ( self::$ob_started === true ) {
598 return;
599 }
600 self::$ob_started = true;
601 // We no longer need this if the handler was started.
602 remove_filter( 'the_content', [$this, 'process_images_from_content'], PHP_INT_MAX );
603
604 ob_start(
605 [&$this, 'replace_content']
606 );
607 }
608
609 /**
610 * Throw error on object clone
611 *
612 * The whole idea of the singleton design pattern is that there is a single
613 * object therefore, we don't want the object to be cloned.
614 *
615 * @codeCoverageIgnore
616 * @access public
617 * @return void
618 * @since 1.0.0
619 */
620 public function __clone() {
621 // Cloning instances of the class is forbidden.
622 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
623 }
624
625 /**
626 * Disable unserializing of the class
627 *
628 * @codeCoverageIgnore
629 * @access public
630 * @return void
631 * @since 1.0.0
632 */
633 public function __wakeup() {
634 // Unserializing instances of the class is forbidden.
635 _doing_it_wrong( __FUNCTION__, esc_html__( 'Cheatin&#8217; huh?', 'optimole-wp' ), '1.0.0' );
636 }
637 }
638