PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 3.0.1
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v3.0.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.0.1, at inc/manager.php

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