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

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