settings = \QWSpeedCache\Plugin::instance()->get_all_settings(); $this->cache_dir = QWSC_CACHE_DIR . 'js/'; $needed = ! empty( $this->settings['minify_js'] ) || ! empty( $this->settings['combine_js'] ) || ! empty( $this->settings['defer_js'] ); if ( ! $needed ) { return; } if ( ! is_dir( $this->cache_dir ) ) { wp_mkdir_p( $this->cache_dir ); } add_action( 'wp_print_scripts', array( $this, 'process' ), 999 ); add_filter( 'script_loader_tag', array( $this, 'filter_tag' ), 10, 3 ); } /** * Exclusions for minify/combine (and shared baseline). * * @return string[] */ private function get_base_excludes() { $rules = JsExclusions::parse_list( $this->settings['exclude_js'] ?? '' ); $safe = ! empty( $this->settings['js_safe_mode'] ); return JsExclusions::merge_safe( $rules, $safe || true ); // always protect jquery family at least via merge when safe; when not safe still add jquery in process. } /** * Exclusions for defer only. * * @return string[] */ private function get_defer_excludes() { $base = JsExclusions::parse_list( $this->settings['exclude_js'] ?? '' ); $defer = JsExclusions::parse_list( $this->settings['defer_js_exclusions'] ?? '' ); $safe = ! empty( $this->settings['js_safe_mode'] ); return JsExclusions::merge_safe( array_merge( $base, $defer ), $safe ); } public function process() { if ( ! \QWSpeedCache\Plugin::should_optimize() || is_admin() || is_customize_preview() ) { return; } // Per-post "Disable minify/combine" is evaluated here, after the main query. if ( \QWSpeedCache\Admin\PostMetabox::flag( 'no_minify' ) ) { return; } global $wp_scripts; if ( empty( $wp_scripts->queue ) ) { return; } $to_process = array(); $excludes = $this->get_base_excludes(); // Always skip jquery family for minify/combine. $excludes = JsExclusions::merge_safe( $excludes, true ); foreach ( $wp_scripts->queue as $handle ) { if ( empty( $wp_scripts->registered[ $handle ] ) ) { continue; } $obj = $wp_scripts->registered[ $handle ]; $src = isset( $obj->src ) ? (string) $obj->src : ''; if ( JsExclusions::is_excluded( $handle, $src, $excludes ) ) { continue; } if ( '' === $src ) { continue; } $to_process[ $handle ] = $obj; } if ( empty( $to_process ) ) { return; } if ( \QWSpeedCache\Optimization\CombineSafety::allow( $this->settings, 'js' ) ) { $to_combine = \QWSpeedCache\Optimization\CombineSafety::filter_items( $to_process, $this->settings, array( $this, 'get_local_path_public' ), 'js' ); if ( count( $to_combine ) >= 2 ) { $this->combine( $to_combine ); } } elseif ( ! empty( $this->settings['minify_js'] ) ) { $this->minify_individual( $to_process ); } } /** * Whether a script tag should never be delayed (modules, already delayed, ld+json, etc.). * * @param string $tag Script HTML. * @return bool */ public static function should_skip_defer_tag( $tag ) { $tag = (string) $tag; if ( '' === $tag ) { return true; } if ( false !== stripos( $tag, 'type="qwsc/delayed"' ) || false !== stripos( $tag, "type='qwsc/delayed'" ) ) { return true; } if ( false !== stripos( $tag, 'type="module"' ) || false !== stripos( $tag, "type='module'" ) ) { return true; } if ( false !== stripos( $tag, 'type="application/ld+json"' ) || false !== stripos( $tag, "type='application/ld+json'" ) ) { return true; } if ( false !== stripos( $tag, 'type="importmap"' ) || false !== stripos( $tag, "type='importmap'" ) ) { return true; } return false; } /** * Add defer attribute if missing (pure). * * @param string $tag Tag HTML. * @return string */ public static function make_deferred_tag( $tag ) { $tag = (string) $tag; if ( false !== stripos( $tag, ' defer' ) || false !== stripos( $tag, ' async' ) ) { return $tag; } if ( self::should_skip_defer_tag( $tag ) ) { return $tag; } $out = preg_replace( '/\s+src=/i', ' defer src=', $tag, 1 ); return is_string( $out ) ? $out : $tag; } public function filter_tag( $tag, $handle, $src ) { if ( ! \QWSpeedCache\Plugin::should_optimize() || is_admin() || empty( $this->settings['defer_js'] ) ) { return $tag; } $src = (string) $src; if ( JsExclusions::is_excluded( $handle, $src, $this->get_defer_excludes() ) ) { return $tag; } return self::make_deferred_tag( $tag ); } /** * Generate an asset safely: never replace a registered original if this * file is absent, empty or could not be written to the cache directory. * * @param string $path Cache file path. * @param string $data Generated asset bytes. * @return bool Whether the generated file is ready for use. */ private static function ensure_cache_asset( $path, $data ) { if ( ! is_string( $data ) || '' === $data ) { return false; } $expected = strlen( $data ); clearstatcache( true, $path ); if ( is_file( $path ) && is_readable( $path ) && filesize( $path ) === $expected ) { return true; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Generated local cache under wp-content/cache; verify the complete write. $written = @file_put_contents( $path, $data, LOCK_EX ); clearstatcache( true, $path ); return $written === $expected && is_file( $path ) && is_readable( $path ) && filesize( $path ) === $expected; } private function minify_individual( $items ) { global $wp_scripts; foreach ( $items as $handle => $obj ) { $src = $this->get_local_path( $obj->src ); if ( ! $src || ! file_exists( $src ) ) { continue; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a validated local enqueued JS asset, never fetching a remote URL. $content = file_get_contents( $src ); if ( false === $content ) { continue; } $min = $this->minify( $content ); $hash = substr( md5( $min ), 0, 12 ); // A registered asset handle is extension-controlled; never use it directly as a filesystem component. $safe_handle = sanitize_file_name( (string) $handle ); if ( '' === $safe_handle ) { $safe_handle = 'asset'; } $out = $this->cache_dir . $safe_handle . '-' . $hash . '.js'; if ( self::ensure_cache_asset( $out, $min ) ) { $wp_scripts->registered[ $handle ]->src = $this->url_from_path( $out ); } } } private function combine( $items ) { global $wp_scripts; $combined = ''; $handles = array(); foreach ( $items as $handle => $obj ) { $src = $this->get_local_path( $obj->src ); if ( ! $src || ! file_exists( $src ) ) { continue; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Reading a validated local enqueued JS asset, never fetching a remote URL. $content = file_get_contents( $src ); if ( false === $content ) { continue; } if ( ! empty( $this->settings['minify_js'] ) ) { $content = $this->minify( $content ); } $combined .= "/* {$handle} */\n" . $content . ";\n"; $handles[] = $handle; } if ( '' === $combined ) { return; } $hash = substr( md5( $combined ), 0, 12 ); $out = $this->cache_dir . 'combined-' . $hash . '.js'; $was_ready = is_file( $out ) && is_readable( $out ) && filesize( $out ) === strlen( $combined ); if ( ! self::ensure_cache_asset( $out, $combined ) ) { return; // Originals stay queued if the generated JS cannot be served. } if ( ! $was_ready ) { do_action( 'qwsc_combined_js_written', $out ); } foreach ( $handles as $handle ) { wp_dequeue_script( $handle ); } wp_enqueue_script( 'qwsc-combined-js', $this->url_from_path( $out ), array( 'jquery' ), QWSC_VERSION, true ); } private function minify( $js ) { return \QWSpeedCache\Optimization\Minify\Engine::js( $js ); } public function get_local_path_public( $src ) { return $this->get_local_path( $src ); } private function get_local_path( $src ) { return LocalAssetPath::resolve( $src ); } private function url_from_path( $path ) { return content_url( 'cache/qw-speed-cache/js/' . basename( $path ) ); } }