*/
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 the page profiler class.
*
* @access public
* @since 1.0.0
* @var Profile Page profiler instance.
*/
public $page_profiler;
/**
* Holds plugin settings.
*
* @var Optml_Settings WordPress settings.
*/
protected $settings;
/**
* Possible integrations with different plugins.
*
* @var array Integrations classes.
*/
private $possible_compatibilities = [
'kadence_blocks',
'essential_blocks',
'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',
'woocommerce',
'smart_search_woocommerce',
'facetwp',
'wp_rest_cache',
'wp_bakery',
'elementor_builder_late',
'wpml',
'otter_blocks',
'spectra',
'wpsp',
'jetengine',
'jetpack',
'jetpack_photon_compatibility',
'wp_rocket',
'wp_super_cache',
'breeze',
'litespeed_cache',
'autoptimize_cache',
'endurance_cache',
'rocketnet',
'speedycache',
'hummingbird',
'aruba_hsc',
'spc',
];
/**
* The current state of the buffer.
*
* @var boolean Buffer state.
*/
private static $ob_started = false;
/**
* The output-buffer nesting level of our capture buffer.
*
* Used to make sure we only ever capture or close our own buffer and not
* one started by a third party.
*
* @var int Buffer nesting level, 0 when no capture buffer is armed.
*/
private static $ob_level = 0;
/**
* Whether the captured buffer was already processed at shutdown.
*
* When true, the fallback output handler passes content through untouched.
*
* @var boolean Processed state.
*/
private static $ob_processed = 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();
self::$instance->page_profiler = new Profile();
add_action( 'after_setup_theme', [ self::$instance, 'init' ] );
add_action( 'wp_footer', [ self::$instance, 'banner' ] );
}
return self::$instance;
}
/**
* The initialize method.
*/
public function init() {
$this->settings = new Optml_Settings();
$added_sizes = $this->settings->get( 'defined_image_sizes' );
foreach ( $added_sizes as $key => $value ) {
add_image_size( $key, $value['width'], $value['height'], true );
}
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->will_load() ) {
if ( $compatibility->should_load_early() ) {
$compatibility->register();
continue;
}
self::$loaded_compatibilities[ $compatibility_class ] = $compatibility;
}
}
if ( ! $this->should_replace() ) {
return;
}
$this->register_hooks();
}
/**
* Render banner.
*
* @return void
*/
public function banner() {
if ( defined( 'REST_REQUEST' ) ) {
return;
}
$has_banner = $this->settings->get( 'banner_frontend' ) === 'enabled';
if ( ! $has_banner ) {
return;
}
$position = $this->settings->get( 'badge_position' ) ?? 'right';
$show_icon_only = $this->settings->get( 'show_badge_icon' ) === 'enabled';
$string = __( 'Optimized by Optimole', 'optimole-wp' );
$div_style = [
'display' => 'flex',
'position' => 'fixed',
'align-items' => 'center',
'bottom' => '15px',
'background-color' => '#fff',
'padding' => '8px 6px',
'font-size' => '12px',
'font-weight' => '600',
'color' => '#444',
'border' => '1px solid #E9E9E9',
'z-index' => '9999999999',
'border-radius' => '4px',
'text-decoration' => 'none',
'font-family' => 'Arial, Helvetica, sans-serif',
];
$div_style[ $position ] = '15px';
$logo = OPTML_URL . 'assets/img/logo.svg';
$link = tsdk_translate_link( 'https://optimole.com/wordpress/?from=badgeOn' );
$css = '';
foreach ( $div_style as $key => $value ) {
$css .= $key . ':' . $value . ';';
}
$output = '';
$output .= '
';
if ( ! $show_icon_only ) {
$output .= '' . esc_html( $string ) . '';
}
$output .= '';
echo $output;
}
/**
* 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 ( apply_filters( 'optml_force_replacement', false ) === true ) {
return true;
}
if ( is_customize_preview() && $this->settings->get( 'offload_media' ) !== 'enabled' ) {
return false;
}
if ( ( is_admin() && ! self::is_ajax_request() ) || ! $this->settings->is_connected() || ! $this->settings->is_enabled() ) {
return false; // @codeCoverageIgnore
}
if ( array_key_exists( 'preview', $_GET ) && ! empty( $_GET['preview'] ) && ! $this->settings->is_offload_enabled() ) {
return false; // @codeCoverageIgnore
}
if ( array_key_exists( 'optml_off', $_GET ) && 'true' === $_GET['optml_off'] ) {
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' ) {
return false; // @codeCoverageIgnore
}
if ( array_key_exists( 'trp-edit-translation', $_GET ) && ( $_GET['trp-edit-translation'] === 'true' || $_GET['trp-edit-translation'] === 'preview' ) ) {
return false; // @codeCoverageIgnore
}
if ( array_key_exists( 'context', $_GET ) && $_GET['context'] === 'edit' ) {
return false; // @codeCoverageIgnore
}
// avada
if ( array_key_exists( 'fb-edit', $_GET ) && ! empty( $_GET['fb-edit'] ) ) {
return false; // @codeCoverageIgnore
}
if ( array_key_exists( 'builder', $_GET ) && ! empty( $_GET['builder'] )
&& array_key_exists( 'builder_id', $_GET ) && ! empty( $_GET['builder_id'] ) ) {
return false; // @codeCoverageIgnore
}
// Motion.page iFrame & builder
if ( ( array_key_exists( 'motionpage_iframe', $_GET ) && $_GET['motionpage_iframe'] === 'true' ) || ( array_key_exists( 'page', $_GET ) && $_GET['page'] === 'motionpage' ) ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison
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' &&
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;
}
}
$filters = $this->settings->get_filters();
return Optml_Filters::should_do_page( $filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_URL ], $filters[ Optml_Settings::FILTER_TYPE_OPTIMIZE ][ Optml_Settings::FILTER_URL_MATCH ] );
}
/**
* 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( 'shutdown', [ $this, 'close_buffer' ], PHP_INT_MIN );
add_action( 'shutdown', [ $this, 'close_final_buffer' ], PHP_INT_MAX );
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' );
}
/**
* Check if we should load the profiler.
*
* @param bool $default_value Default value.
*
* @return bool Should load the profiler.
*/
public static function should_load_profiler( $default_value = false ) {
return ! $default_value && apply_filters( 'optml_page_profiler_disable', false ) === false;
}
/**
* Decide if the temporary Cache-Control header can be sent while page profiling is pending.
*
* The header must never be sent on non-cacheable pages: it would replace a
* Cache-Control header already set by WordPress or another plugin (PHP's
* header() replaces same-name headers by default), e.g. WooCommerce's
* no-cache header on cart and checkout, letting proxies cache user-specific
* pages. We back off when DONOTCACHEPAGE is set or when any Cache-Control
* header exists already, and let developers override the decision.
*
* @param array|null $sent_headers Headers already set for the response; defaults to headers_list().
* @param bool|null $do_not_cache Whether the page is flagged as non-cacheable; defaults to the DONOTCACHEPAGE constant.
*
* @return bool Whether the header can be sent.
*/
public function should_send_temporary_cache_header( $sent_headers = null, $do_not_cache = null ) {
if ( null === $do_not_cache ) {
$do_not_cache = defined( 'DONOTCACHEPAGE' ) && DONOTCACHEPAGE;
}
$send = ! $do_not_cache;
if ( $send ) {
if ( null === $sent_headers ) {
$sent_headers = headers_list();
}
foreach ( $sent_headers as $header ) {
if ( stripos( $header, 'cache-control:' ) === 0 ) {
$send = false;
break;
}
}
}
/**
* Filters whether the temporary `Cache-Control: max-age=300` header is sent
* while page profiling is pending for the current page.
*
* @param bool $send Computed decision: false when DONOTCACHEPAGE is set or a Cache-Control header exists already.
*/
return apply_filters( 'optml_send_temporary_cache_header', $send ) === true;
}
/**
* Filter raw HTML content for urls.
*
* @param string $html HTML to filter.
* @param bool $partial If this is a partial content replacement and not a full page. It matters when we are are doing full page optimization like viewport lazyload.
*
* @return mixed Filtered content.
*/
public function replace_content( $html, $partial = false ) {
if ( defined( 'REST_REQUEST' ) && REST_REQUEST && is_user_logged_in() && ( apply_filters( 'optml_force_replacement', false ) !== true ) ) {
return $html;
}
if ( self::should_load_profiler( $partial ) ) {
$profile_id = Profile::generate_id( $html );
$should_show_comment = false;
// We disable the optimizer for logged in users.
if ( ! is_user_logged_in() || ! apply_filters( 'optml_force_page_profiler', false ) !== true ) {
$js_optimizer = Optml_Admin::get_optimizer_script( false );
if ( ! $this->page_profiler->exists_all( $profile_id ) ) {
$missing = $this->page_profiler->missing_devices( $profile_id );
$time = time();
$url = esc_url( esc_js( $this->get_current_url() ) );
$hmac = wp_hash( $profile_id . $time . $url, 'nonce' );
$js_optimizer = str_replace(
[ Profile::PLACEHOLDER, Profile::PLACEHOLDER_MISSING, Profile::PLACEHOLDER_TIME, Profile::PLACEHOLDER_HMAC, Profile::PLACEHOLDER_URL ],
[ $profile_id, implode( ',', $missing ), strval( $time ), $hmac, $url ],
$js_optimizer
);
$html = str_replace( Optml_Admin::get_optimizer_script( true ), $js_optimizer, $html );
if ( ! headers_sent() && $this->should_send_temporary_cache_header() ) {
header( 'Cache-Control: max-age=300' ); // Attempt to cache the page just for 5 mins until the optimizer is done. Once the optimizer is done, the page will load optimized.
}
} else {
$should_show_comment = isset( $_GET['optml_debug'] ) && $_GET['optml_debug'] === 'true';
}
}
Profile::set_current_profile_id( $profile_id );
$this->page_profiler->set_current_profile_data();
if ( $should_show_comment ) {
$html = str_replace( '