protected_scripts = $this->get_protected_scripts(); if ( wpsco_get_setting( 'js_minify', false ) ) { add_action( 'wp_enqueue_scripts', array( $this, 'process_scripts' ), 999999 ); } if ( wpsco_get_setting( 'js_defer', false ) ) { add_filter( 'script_loader_tag', array( $this, 'add_defer' ), 10, 3 ); } if ( wpsco_get_setting( 'js_delay', false ) ) { add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_delay_script' ) ); add_filter( 'script_loader_tag', array( $this, 'maybe_delay_script' ), 20, 3 ); } } /** * Get list of protected script handles that must never be optimized. * * @return array */ private function get_protected_scripts() { $protected = array( // jQuery. 'jquery', 'jquery-core', 'jquery-migrate', // WordPress Core utilities. 'underscore', 'wp-util', 'wp-hooks', 'wp-i18n', 'wp-api-fetch', 'wp-polyfill', 'regenerator-runtime', // Sliders & carousels (crucial for hero banners). 'slick', 'slick-theme', 'swiper', 'owl-carousel', 'flexslider', 'imagesloaded', 'masonry', // WooCommerce critical. 'wc-checkout', 'wc-cart', 'wc-add-to-cart', 'wc-add-to-cart-variation', 'wc-single-product', 'wc-cart-fragments', 'woocommerce', 'wc-gateway-stripe', // Payment gateways. 'stripe', 'stripe_v3', 'razorpay', 'razorpay_checkout', 'paypal', 'ppcp-smart-button', 'ppcp-gateway', // reCAPTCHA. 'recaptcha', 'google-recaptcha', 'wpcf7-recaptcha', // Elementor. 'elementor', 'elementor-pro', 'elementor-frontend', 'elementor-pro-frontend', 'elementor-frontend-modules', 'elementor-webpack-runtime', 'elementor-waypoints', // WordPress admin. 'admin-bar', 'wp-auth-check', 'heartbeat', ); // Merge with user auto-exclusions. $auto_exclusions = wpsco_get_setting( 'js_auto_exclusions', array() ); $user_exclusions = wpsco_get_setting( 'js_exclusions', array() ); return array_unique( array_merge( $protected, $auto_exclusions, $user_exclusions ) ); } /** * Check if a script handle is protected. * * @param string $handle Script handle. * @return bool */ private function is_protected( $handle ) { // Direct match. if ( in_array( $handle, $this->protected_scripts, true ) ) { return true; } // Partial match for common patterns. $partial_patterns = array( 'wc-', 'woocommerce', 'stripe', 'razorpay', 'paypal', 'recaptcha', 'captcha', 'elementor', 'admin', 'slick', 'swiper', 'slider', 'carousel', 'underscore', 'wp-util', 'wp-hooks', ); foreach ( $partial_patterns as $pattern ) { if ( stripos( $handle, $pattern ) !== false ) { return true; } } return false; } /** * Process enqueued scripts for minification. */ /** * Process enqueued scripts for in-memory inline minification. * * Safely minifies inline script blocks and localization payloads without * writing any dynamic executable JavaScript files to disk (strictly adhering * to WordPress Plugin Security Guidelines). */ public function process_scripts() { if ( is_admin() ) { return; } global $wp_scripts; if ( ! $wp_scripts instanceof WP_Scripts ) { return; } foreach ( $wp_scripts->queue as $handle ) { if ( ! isset( $wp_scripts->registered[ $handle ] ) ) { continue; } if ( $this->is_protected( $handle ) ) { continue; } $script = $wp_scripts->registered[ $handle ]; // Safely minify registered inline script data in memory without generating dynamic JS files on disk. if ( ! empty( $script->extra['data'] ) && is_string( $script->extra['data'] ) ) { $minified = $this->minify_js( $script->extra['data'] ); if ( false !== $minified && ! empty( $minified ) ) { $wp_scripts->registered[ $handle ]->extra['data'] = $minified; } } if ( ! empty( $script->extra['after'] ) && is_array( $script->extra['after'] ) ) { foreach ( $script->extra['after'] as $idx => $inline ) { if ( is_string( $inline ) ) { $minified = $this->minify_js( $inline ); if ( false !== $minified && ! empty( $minified ) ) { $wp_scripts->registered[ $handle ]->extra['after'][ $idx ] = $minified; } } } } if ( ! empty( $script->extra['before'] ) && is_array( $script->extra['before'] ) ) { foreach ( $script->extra['before'] as $idx => $inline ) { if ( is_string( $inline ) ) { $minified = $this->minify_js( $inline ); if ( false !== $minified && ! empty( $minified ) ) { $wp_scripts->registered[ $handle ]->extra['before'][ $idx ] = $minified; } } } } } } /** * Minify JavaScript content. * * Safe minification: removes comments and excess whitespace. * Does NOT alter logic, variable names, or structure. * * @param string $js JavaScript content. * @return string|false Minified JS or false on failure. */ public function minify_js( $js ) { if ( empty( $js ) ) { return ''; } try { // Preserve strings and regex literals. $preserved = array(); $index = 0; // Preserve template literals. $js = preg_replace_callback( '/`(?:[^`\\\\]|\\\\.)*`/s', function ( $matches ) use ( &$preserved, &$index ) { $placeholder = '"___WPSCO_TMPL_' . $index . '___"'; $preserved[ $placeholder ] = $matches[0]; $index++; return $placeholder; }, $js ); // Preserve double-quoted strings. $js = preg_replace_callback( '/"(?:[^"\\\\]|\\\\.)*"/s', function ( $matches ) use ( &$preserved, &$index ) { $placeholder = '"___WPSCO_DQ_' . $index . '___"'; $preserved[ $placeholder ] = $matches[0]; $index++; return $placeholder; }, $js ); // Preserve single-quoted strings. $js = preg_replace_callback( "/\x27(?:[^\x27\\\\]|\\\\.)*\x27/s", function ( $matches ) use ( &$preserved, &$index ) { $placeholder = "'___WPSCO_SQ_" . $index . "___'"; $preserved[ $placeholder ] = $matches[0]; $index++; return $placeholder; }, $js ); // Safe comment stripping: remove block comments (preserving conditional /*! comments). $js = preg_replace( '/\/\*(?!\!)[^*]*\*+(?:[^/*][^*]*\*+)*\//', '', $js ); // Strip trailing whitespace from ends of lines without removing line breaks. // Preserving line breaks guarantees Automatic Semicolon Insertion (ASI) never breaks. $js = preg_replace( '/[ \t]+$/m', '', $js ); // Collapse excessive blank lines. $js = preg_replace( '/\n{3,}/', "\n\n", $js ); // Restore preserved content. foreach ( $preserved as $placeholder => $original ) { $js = str_replace( $placeholder, $original, $js ); } return trim( $js ); } catch ( \Exception $e ) { wpsco_log( 'JS minification error: ' . $e->getMessage(), 'error' ); return false; } } /** * Add defer attribute to script tags. * * @param string $tag Script tag. * @param string $handle Script handle. * @param string $src Script source. * @return string */ public function add_defer( $tag, $handle, $src ) { if ( is_admin() ) { return $tag; } if ( $this->is_protected( $handle ) ) { return $tag; } // Never defer theme scripts to ensure UI elements (sliders, menus, customizers) run immediately. if ( strpos( $src, '/wp-content/themes/' ) !== false || strpos( $src, '/themes/' ) !== false ) { return $tag; } // Don't double-add. if ( strpos( $tag, 'defer' ) !== false || strpos( $tag, 'async' ) !== false ) { return $tag; } return str_replace( ' src=', ' defer src=', $tag ); } /** * Maybe delay a script tag. * * Only delays heavy external tracking, analytics, and marketing scripts * (GA4, GTM, FB Pixel, Clarity, TikTok, Hotjar, Chat widgets). * Never delays WordPress core, theme, Elementor, WooCommerce, or slider scripts. * * @param string $tag Script tag. * @param string $handle Script handle. * @param string $src Script source. * @return string */ public function maybe_delay_script( $tag, $handle, $src ) { if ( is_admin() ) { return $tag; } if ( $this->is_protected( $handle ) ) { return $tag; } // Never delay internal theme scripts or WordPress core scripts. if ( strpos( $src, '/wp-content/themes/' ) !== false || strpos( $src, '/wp-includes/' ) !== false ) { return $tag; } // Check if handle or src matches user explicit delay list. $user_delays = wpsco_get_setting( 'js_delay_list', array() ); $should_delay = false; if ( ! empty( $user_delays ) && is_array( $user_delays ) ) { foreach ( $user_delays as $delay_item ) { if ( ! empty( $delay_item ) && ( stripos( $handle, $delay_item ) !== false || stripos( $src, $delay_item ) !== false ) ) { $should_delay = true; break; } } } // Default heavy marketing/analytics trackers to delay (matches enqueued scripts; does not connect externally). // Fully disclosed in readme.txt under External Services. if ( ! $should_delay ) { $trackers = array( 'googletagmanager.com', 'google-analytics.com', 'gtm.js', 'analytics.js', 'connect.facebook.net', 'fbevents.js', 'clarity.ms', 'static.hotjar.com', 'analytics.tiktok.com', 'pintrk', 'tawk.to', 'crisp.chat', ); foreach ( $trackers as $tracker ) { if ( stripos( $src, $tracker ) !== false || stripos( $handle, $tracker ) !== false ) { $should_delay = true; break; } } } if ( ! $should_delay ) { return $tag; // Keep internal and library scripts loading normally. } // Change type to prevent execution until interaction or timeout. $tag = str_replace( "type='text/javascript'", "type='wpsco/delayed'", $tag ); $tag = str_replace( 'type="text/javascript"', 'type="wpsco/delayed"', $tag ); if ( strpos( $tag, 'type=' ) === false ) { $tag = str_replace( '