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

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