*/ final class Optml_Manager { /** * Holds allowed compatibilities objects. * * @var Optml_compatibility[] Compatibilities objects. */ public static $loaded_compatibilities = []; /** * Cached object instance. * * @var Optml_Manager */ protected static $instance = null; /** * Holds the url replacer class. * * @access public * @since 1.0.0 * @var Optml_Url_Replacer Replacer instance. */ public $url_replacer; /** * Holds the tag replacer class. * * @access public * @since 1.0.0 * @var Optml_Tag_Replacer Replacer instance. */ public $tag_replacer; /** * Holds the lazyload replacer class. * * @access public * @since 1.0.0 * @var Optml_Lazyload_Replacer Replacer instance. */ public $lazyload_replacer; /** * Holds plugin settings. * * @var Optml_Settings WordPress settings. */ protected $settings; /** * Possible integrations with different plugins. * * @var array Integrations classes. */ private $possible_compatibilities = [ 'shortcode_ultimate', 'foogallery', 'envira', 'beaver_builder', 'jet_elements', 'revslider', 'metaslider', 'essential_grid', 'yith_quick_view', 'cache_enabler', 'elementor_builder', 'divi_builder', 'thrive', 'master_slider', 'pinterest', 'sg_optimizer', 'wp_fastest_cache', 'swift_performance', 'w3_total_cache', 'translate_press', 'give_wp', 'smart_search_woocommerce', ]; /** * The current state of the buffer. * * @var boolean Buffer state. */ private static $ob_started = false; /** * Class instance method. * * @codeCoverageIgnore * @static * @return Optml_Manager * @since 1.0.0 * @access public */ public static function instance() { if ( null === self::$instance ) { self::$instance = new self(); self::$instance->url_replacer = Optml_Url_Replacer::instance(); self::$instance->tag_replacer = Optml_Tag_Replacer::instance(); self::$instance->lazyload_replacer = Optml_Lazyload_Replacer::instance(); add_action( 'after_setup_theme', [ self::$instance, 'init' ] ); } return self::$instance; } /** * The initialize method. */ public function init() { $this->settings = new Optml_Settings(); foreach ( $this->possible_compatibilities as $compatibility_class ) { $compatibility_class = 'Optml_' . $compatibility_class; $compatibility = new $compatibility_class; /** * Check if we should load compatibility. * * @var Optml_compatibility $compatibility Class to register. */ if ( $compatibility->should_load() ) { if ( $compatibility->should_load_early() ) { $compatibility->register(); continue; } self::$loaded_compatibilities[ $compatibility_class ] = $compatibility; } } if ( ! $this->should_replace() ) { return; } $this->register_hooks(); } /** * Check if we should rewrite the urls. * * @return bool If we can replace the image. */ public function should_replace() { if ( apply_filters( 'optml_should_replace_page', false ) ) { return false; } if ( ( is_admin() && ! self::is_ajax_request() ) || ! $this->settings->is_connected() || ! $this->settings->is_enabled() || is_customize_preview() ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'preview', $_GET ) && ! empty( $_GET['preview'] ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'optml_off', $_GET ) && 'true' == $_GET['optml_off'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison return false; // @codeCoverageIgnore } if ( array_key_exists( 'elementor-preview', $_GET ) && ! empty( $_GET['elementor-preview'] ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'ct_builder', $_GET ) && ! empty( $_GET['ct_builder'] ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'et_fb', $_GET ) && ! empty( $_GET['et_fb'] ) ) { return false; // @codeCoverageIgnore } if ( array_key_exists( 'tve', $_GET ) && $_GET['tve'] == 'true' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison return false; // @codeCoverageIgnore } if ( array_key_exists( 'trp-edit-translation', $_GET ) && ( $_GET['trp-edit-translation'] == 'true' || $_GET['trp-edit-translation'] == 'preview' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison return false; // @codeCoverageIgnore } if ( array_key_exists( 'context', $_GET ) && $_GET['context'] == 'edit' ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison return false; // @codeCoverageIgnore } if ( array_key_exists( 'fb-edit', $_GET ) && ! empty( $_GET['fb-edit'] ) ) { return false; // @codeCoverageIgnore } /** * Disable replacement on POST request and when user is logged in, but allows for sample image call widget in dashboard */ if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] == 'POST' && // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison is_user_logged_in() && ( ! isset( $_GET['quality'] ) || ! current_user_can( 'manage_options' ) ) ) { return false; // @codeCoverageIgnore } if ( class_exists( 'FLBuilderModel', false ) ) { $post_data = FLBuilderModel::get_post_data(); if ( isset( $_GET['fl_builder'] ) || isset( $post_data['fl_builder'] ) ) { return false; } } return Optml_Filters::should_do_page( $this->settings->get_filters()[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_URL ] ); } /** * Check if we are in a ajax contex where we should enable replacement. * * @return bool Is ajax request? */ public static function is_ajax_request() { if ( apply_filters( 'optml_force_replacement_on', false ) === true ) { return true; } if ( ! function_exists( 'is_user_logged_in' ) ) { return false; } // Disable for logged in users to avoid unexpected results. if ( is_user_logged_in() ) { return false; } if ( ! function_exists( 'wp_doing_ajax' ) ) { return false; } if ( ! wp_doing_ajax() ) { return false; } if ( isset( $_REQUEST['action'] ) && strpos( $_REQUEST['action'], 'wpmdb' ) !== false ) { return false; } return true; } /** * Register frontend replacer hooks. */ public function register_hooks() { do_action( 'optml_replacer_setup' ); if ( $this->settings->get( 'native_lazyload' ) === 'disabled' ) { add_filter( 'wp_lazy_loading_enabled', '__return_false' ); } add_filter( 'the_content', [ $this, 'process_images_from_content' ], PHP_INT_MAX ); /** * When we have to process cdn images, i.e MIRROR is defined, * we need this as late as possible for other replacers to occur. * Otherwise, we can hook first to avoid any other plugins to take care of replacement. */ add_action( self::is_ajax_request() ? 'init' : 'template_redirect', [ $this, 'process_template_redirect_content', ], defined( 'OPTML_SITE_MIRROR' ) ? PHP_INT_MAX : PHP_INT_MIN ); add_action( 'template_redirect', [ $this, 'register_after_setup' ] ); add_action( 'rest_api_init', [ $this, 'process_template_redirect_content' ], PHP_INT_MIN ); add_action( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX, 4 ); foreach ( self::$loaded_compatibilities as $registered_compatibility ) { $registered_compatibility->register(); } } /** * Run after Optimole is fully setup. */ public function register_after_setup() { do_action( 'optml_after_setup' ); } /** * Replace urls in post meta values. * * @param mixed $metadata Metadata. * @param int $object_id Post id. * @param string $meta_key Meta key. * @param bool $single Is single. * * @return mixed Altered meta. */ public function replace_meta( $metadata, $object_id, $meta_key, $single ) { $meta_needed = '_elementor_data'; if ( isset( $meta_key ) && $meta_needed === $meta_key ) { remove_filter( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX ); $current_meta = get_post_meta( $object_id, $meta_needed, $single ); add_filter( 'get_post_metadata', [ $this, 'replace_meta' ], PHP_INT_MAX, 4 ); if ( ! is_string( $current_meta ) ) { return $metadata; } return $this->replace_content( $current_meta ); } // Return original if the check does not pass return $metadata; } /** * Filter raw HTML content for urls. * * @param string $html HTML to filter. * * @return mixed Filtered content. */ public function replace_content( $html ) { if ( defined( 'REST_REQUEST' ) && REST_REQUEST && is_user_logged_in() ) { return $html; } $html = $this->add_html_class( $html ); $html = $this->process_images_from_content( $html ); if ( $this->settings->get( 'video_lazyload' ) === 'enabled' ) { $html = apply_filters( 'optml_video_replace', $html ); if ( Optml_Lazyload_Replacer::found_iframe() === true ) { if ( strpos( $html, Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT ) !== false ) { $html = str_replace( Optml_Lazyload_Replacer::IFRAME_TEMP_COMMENT, Optml_Lazyload_Replacer::IFRAME_PLACEHOLDER_CLASS, $html ); } else { $html = preg_replace( '/
(.*)<\/head>/ism', ' $1' . Optml_Lazyload_Replacer::IFRAME_PLACEHOLDER_STYLE . '', $html ); } } } $html = apply_filters( 'optml_url_pre_process', $html ); $html = $this->process_urls_from_content( $html ); $html = apply_filters( 'optml_url_post_process', $html ); return $html; } /** * Adds a filter that allows adding classes to the HTML tag. * * @param string $content The HTML content. * * @return mixed */ public function add_html_class( $content ) { if ( empty( $content ) ) { return $content; } $additional_html_classes = apply_filters( 'optml_additional_html_classes', [] ); if ( ! $additional_html_classes ) { return $content; } if ( preg_match( '/