setup(); } return $instance; } /** * Setup routine */ public function setup() { $this->settings = \PoweredCache\Utils\get_settings(); // Check request method if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || ! in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD' ], true ) ) { return true; } add_action( 'plugins_loaded', [ $this, 'setup_file_optimizer' ] ); } /** * Setup file optimizer module */ public function setup_file_optimizer() { /** * Filters whether apply or not apply file optimizations on wp-admin. * * @hook powered_cache_fo_dashboard * * @param {boolean} true to enable optimizations for dashboard. * * @return {boolean} New value. * @since 2.0 */ $this->optimize_dashboard = apply_filters( 'powered_cache_fo_dashboard', false ); add_action( 'powered_cache_purge_all_cache', [ $this, 'maybe_purge_fo_cache' ] ); /** * Don't optimize wp-admin by default * This might worth to add as option? */ if ( is_admin() && ! $this->optimize_dashboard ) { return; } /** * Don't optimize in customizer preview */ if ( ! empty( $_GET['customize_changeset_uuid'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended \PoweredCache\Utils\log( 'Do not run file optimizer in customizer preview' ); return; } /** * Filters FileOptimizer integration * * @hook powered_cache_fo_disable * * @param {boolean} False by default. * * @return {boolean} New value. * @since 2.2 */ $disable_file_optimizer = apply_filters( 'powered_cache_fo_disable', false ); if ( $disable_file_optimizer ) { return; } add_action( 'init', [ $this, 'setup_css_combine' ] ); add_action( 'init', [ $this, 'setup_js_combine' ] ); add_filter( 'script_loader_tag', [ $this, 'js_minify' ], 10, 3 ); add_filter( 'style_loader_tag', [ $this, 'css_minify' ], 10, 4 ); add_filter( 'powered_cache_fo_script_loader_tag', [ $this, 'change_js_execute_method' ] ); add_filter( 'script_loader_tag', [ $this, 'change_js_execute_method' ], 99 ); add_action( 'after_setup_theme', [ $this, 'maybe_start_buffer' ], 999 ); add_action( 'template_redirect', [ $this, 'maybe_suppress_optimizations' ] ); if ( ! $this->settings['combine_js'] ) { add_filter( 'powered_cache_fo_js_do_concat', '__return_false' ); } if ( ! $this->settings['combine_css'] ) { add_filter( 'powered_cache_fo_css_do_concat', '__return_false' ); } if ( $this->settings['combine_google_fonts'] ) { add_action( 'wp_enqueue_scripts', [ $this, 'combine_google_fonts' ], 99 ); } } /** * Process output buffer * * @return void */ public function maybe_start_buffer() { if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return; } $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); if ( strpos( $request_uri, 'robots.txt' ) !== false || strpos( $request_uri, '.htaccess' ) !== false ) { return; } if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) { return; } $file_extension = preg_replace( '#^(.*?)\?.*$#', '$1', $request_uri ); $file_extension = trim( preg_replace( '#^.*\.(.*)$#', '$1', $file_extension ) ); if ( ! preg_match( '#index\.php$#i', $request_uri ) && in_array( $file_extension, array( 'php', 'xml', 'xsl' ), true ) ) { return; } if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { return; } ob_start( [ $this, 'process_buffer' ] ); } /** * Process output buffer * * @param string $html Output buffer * * @return string|string[]|null */ public function process_buffer( $html ) { $html = $this->maybe_defer_inline_scripts( $html ); $html = $this->maybe_delay_scripts( $html ); $html = $this->maybe_replace_google_fonts_with_bunny_fonts( $html ); $html = $this->maybe_minify_html( $html ); return $html; } /** * Replace Google Fonts with Bunny drop-in replacement's * * @param string $html Output buffer * * @return array|mixed|string|string[] * @since 3.0 */ public function maybe_replace_google_fonts_with_bunny_fonts( $html ) { if ( ! $this->settings['use_bunny_fonts'] ) { return $html; } $html = str_replace( [ 'https://fonts.googleapis.com', 'http://fonts.googleapis.com', '//fonts.googleapis.com', ], 'https://fonts.bunny.net', $html ); return $html; } /** * Maybe suppress optimizations based on the post meta options * * @since 2.1 */ public function maybe_suppress_optimizations() { if ( is_singular() ) { $disable_css_optimization = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_CSS_OPTIMIZATION, true ); $disable_js_optimization = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_JS_OPTIMIZATION, true ); $disable_js_defer = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_JS_DEFER, true ); $disable_js_delay = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_JS_DELAY, true ); if ( $disable_css_optimization ) { add_filter( 'powered_cache_fo_css_do_concat', '__return_false' ); add_filter( 'powered_cache_fo_disable_css_minify', '__return_true' ); } if ( $disable_js_optimization ) { add_filter( 'powered_cache_fo_js_do_concat', '__return_false' ); add_filter( 'powered_cache_fo_disable_js_minify', '__return_true' ); } if ( $disable_js_defer ) { add_filter( 'powered_cache_disable_js_defer', '__return_true' ); add_filter( 'powered_cache_disable_js_defer_inline', '__return_true' ); } if ( $disable_js_delay ) { add_filter( 'powered_cache_delayed_js_skip', '__return_true' ); } } } /** * Purge FO cache * * @since 2.0 */ public function maybe_purge_fo_cache() { if ( file_exists( POWERED_CACHE_FO_CACHE_DIR ) ) { remove_dir( POWERED_CACHE_FO_CACHE_DIR ); } } /** * Minify given HTML output * * @param string $buffer HTML * * @return string|string[]|null */ public function maybe_minify_html( $buffer ) { if ( ! $this->settings['minify_html'] ) { return $buffer; } if ( false === stripos( $buffer, 'doOptimizeViaHtmlDomParser( $this->settings['minify_html_dom_optimization'] ); $html_min->doRemoveOmittedQuotes( false ); $html_min->doRemoveOmittedHtmlTags( false ); $html_min->overwriteSpecialScriptTags( [ 'x-tmpl-mustache', 'text/template' ] ); $buffer = $html_min->minify( $buffer ); if ( ! $this->settings['minify_html_dom_optimization'] ) { // Remove line breaks and spaces between tags $buffer = preg_replace( '/>\s+<', $buffer ); // Remove comments $buffer = preg_replace( '//', '', $buffer ); } return $buffer; } /** * Add JS execution method to script tag * * @param string $tag settings['js_defer']; if ( ! $is_defer ) { return $tag; } if ( preg_match( '/]*\b(?:async|defer)\b[^>]*>/i', $tag ) ) { return $tag; } if ( Helper::is_defer_excluded( $tag ) ) { return $tag; } /** * Filters whether disable or not disable Defer * * @hook powered_cache_disable_js_defer * * @param {boolean} true to disable defer * * @return {boolean} New value. * @since 3.2 */ if ( apply_filters( 'powered_cache_disable_js_defer', false, $tag ) ) { return $tag; } $search = '' . PHP_EOL; /** * Delayed JS script content * * @hook powered_cache_delayed_js_script_content * * @param {string} $delay_js_script_content Delayed JS script content * @param {string} $script_path Script path * @param {string} $html HTML buffer * @param {int} $delay_timeout Delay time in milliseconds * * @return {string} New value. * @since 3.4 */ $delay_js_script_content = apply_filters( 'powered_cache_delayed_js_script_content', $delay_js_script_content, $script_path, $html, $delay_timeout ); $html = substr_replace( $html, $delay_js_script_content, $head_pos, 0 ); $script_loader = PHP_EOL . '' . PHP_EOL; /** * Delayed JS script loader content * * @hook powered_cache_delayed_js_script_loader * * @param {string} $script_loader Delayed JS script loader content * @param {string} $html HTML buffer * @param {int} $delay_timeout Delay time in milliseconds * * @return {string} New value. * @since 3.4 */ $script_loader = apply_filters( 'powered_cache_delayed_js_script_loader', $script_loader, $html, $delay_timeout ); $body_pos = strpos( $html, '' ); if ( false !== $body_pos ) { $html = substr_replace( $html, $script_loader, $body_pos, 0 ); } else { $html_pos = strpos( $html, '' ); if ( false !== $html_pos ) { $html = substr_replace( $html, $script_loader, $html_pos, 0 ); } } return $html; } /** * Defer jQuery depended inline scripts * * @param string $html Output buffer * * @return array|mixed|string|string[]|null * @since 3.2 */ public function maybe_defer_inline_scripts( $html ) { if ( ! $this->settings['js_defer'] ) { return $html; } $html = preg_replace_callback( '/(]*>)(.*?)<\/script>/s', function ( $matches ) { $script_open_tag = $matches[1]; $script_content = $matches[2]; if ( ! $this->should_defer_script( $script_content, $script_open_tag ) ) { return $matches[0]; // Return the script unchanged } /** * Filters whether disable or not disable inline defer * * @hook powered_cache_disable_js_defer_inline * * @param {boolean} true to disable defer * * @return {boolean} New value. * @since 3.2 */ if ( apply_filters( 'powered_cache_disable_js_defer_inline', false, $script_content, $script_open_tag ) ) { return $matches[0]; // Return the script unchanged } return $script_open_tag . 'window.addEventListener("DOMContentLoaded", function() {(function($) {' . $script_content . '})(jQuery);});'; }, $html ); return $html; } /** * Determine whether defer or not defer inline script * * @param string $script_content Script content * @param string $script_attributes Script attributes * * @return bool * @since 3.2 */ private function should_defer_script( $script_content, $script_attributes ) { // Ignore scripts with a 'src' attribute (external scripts) if ( false !== strpos( $script_attributes, 'src=' ) ) { return false; } // ignore ld+json if ( false !== strpos( $script_attributes, 'type="application/ld+json"' ) ) { return false; } // Check if the script contains 'DOMContentLoaded' or 'document.write' if ( false !== strpos( $script_content, 'DOMContentLoaded' ) || false !== strpos( $script_content, 'document.write' ) ) { return false; } // Check if the script does NOT contain jQuery-related functions if ( false === strpos( $script_content, 'jQuery(' ) && false === strpos( $script_content, '$.(' ) && false === strpos( $script_content, '$(' ) ) { return false; } return true; } }