0, 'bytes_saved' => 0, 'processing_time' => 0, ); /** * Constructor * * @since 1.0.0 */ public function __construct() { $upload_dir = wp_upload_dir(); $this->cache_dir = $upload_dir['basedir'] . '/accessibility-audio-tts/cache/assets/'; $this->ensure_cache_directory(); } /** * Get minified asset URL. * * @since 1.0.0 * @param string $src Original asset URL. * @param string $type Asset type (script/style). * @return string|false Minified URL or false if not available. */ public function get_minified_url( string $src, string $type ) { $minified_filename = $this->get_minified_filename( $src, $type ); $minified_path = $this->cache_dir . $minified_filename; if ( file_exists( $minified_path ) ) { return $this->get_cache_url() . $minified_filename; } return false; } /** * Create minified version of asset. * * @since 1.0.0 * @param string $src Asset source URL. * @param string $type Asset type (script/style). * @return string Minified asset URL. */ public function create_minified_version( string $src, string $type ): string { $start_time = microtime( true ); // Get original file content. $original_content = $this->get_asset_content( $src ); if ( ! $original_content ) { return $src; // Return original if can't read. } // Minify content. $minified_content = $this->minify_content( $original_content, $type ); // Save minified version. $minified_filename = $this->get_minified_filename( $src, $type ); $minified_path = $this->cache_dir . $minified_filename; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- local cache write intended. if ( file_put_contents( $minified_path, $minified_content ) ) { // Update statistics. ++$this->stats['minified_files']; $this->stats['bytes_saved'] += strlen( $original_content ) - strlen( $minified_content ); $this->stats['processing_time'] += microtime( true ) - $start_time; return $this->get_cache_url() . $minified_filename; } return $src; // Return original if can't save. } /** * Minify asset content. * * @since 1.0.0 * @param string $content Asset content. * @param string $type Asset type (script/style). * @return string Minified content. */ public function minify_asset( string $content, string $type ): string { return $this->minify_content( $content, $type ); } /** * Minify content based on type. * * @since 1.0.0 * @param string $content Content to minify. * @param string $type Content type (script/style). * @return string Minified content. */ private function minify_content( string $content, string $type ): string { switch ( $type ) { case 'script': return $this->minify_javascript( $content ); case 'style': return $this->minify_css( $content ); default: return $content; } } /** * Minify JavaScript content. * * @since 1.0.0 * @param string $js JavaScript content. * @return string Minified JavaScript. */ private function minify_javascript( string $js ): string { // Remove comments (but preserve license comments). $js = preg_replace( '/\/\*(?![!*]).*?\*\//s', '', $js ); $js = preg_replace( '/\/\/(?![!\/]).*$/m', '', $js ); // Remove unnecessary whitespace. $js = preg_replace( '/\s+/', ' ', $js ); // Remove whitespace around operators and punctuation. $js = preg_replace( '/\s*([{}();,:])\s*/', '$1', $js ); // Remove trailing semicolons before }. $js = preg_replace( '/;+}/', '}', $js ); return trim( $js ); } /** * Minify CSS content. * * @since 1.0.0 * @param string $css CSS content. * @return string Minified CSS. */ private function minify_css( string $css ): string { // Remove comments (but preserve license comments). $css = preg_replace( '/\/\*(?![!*]).*?\*\//s', '', $css ); // Remove unnecessary whitespace. $css = preg_replace( '/\s+/', ' ', $css ); // Remove whitespace around CSS syntax. $css = preg_replace( '/\s*([{}:;,>+~])\s*/', '$1', $css ); // Remove trailing semicolon before }. $css = preg_replace( '/;+}/', '}', $css ); // Remove unnecessary quotes from URLs. $css = preg_replace( '/url\((["\'])([^"\']*)\1\)/', 'url($2)', $css ); // Convert hex colors to shorter form when possible. $css = preg_replace( '/#([0-9a-f])\1([0-9a-f])\2([0-9a-f])\3/i', '#$1$2$3', $css ); return trim( $css ); } /** * Get asset content from URL or file path. * * @since 1.0.0 * @param string $src Asset source. * @return string|false Asset content or false on failure. */ private function get_asset_content( string $src ) { // Convert URL to file path if it's a local asset. if ( strpos( $src, ACCESSIBILITY_AUDIO_TTS_PLUGIN_URL ) === 0 ) { $file_path = str_replace( ACCESSIBILITY_AUDIO_TTS_PLUGIN_URL, ACCESSIBILITY_AUDIO_TTS_PLUGIN_DIR, $src ); if ( file_exists( $file_path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- local plugin file read. return file_get_contents( $file_path ); } } // Try to fetch remote content (with timeout). $response = wp_remote_get( $src, array( 'timeout' => 10, 'user-agent' => 'Accessibility-Audio-TTS Asset Optimizer', ) ); if ( is_wp_error( $response ) ) { return false; } $code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { return false; } return wp_remote_retrieve_body( $response ); } /** * Generate minified filename. * * @since 1.0.0 * @param string $src Original asset URL. * @param string $type Asset type. * @return string Minified filename. */ private function get_minified_filename( string $src, string $type ): string { $hash = md5( $src ); $extension = ( 'script' === $type ) ? 'js' : 'css'; return "accessibility-audio-tts-audio-{$hash}.min.{$extension}"; } /** * Get cache directory URL. * * @since 1.0.0 * @return string Cache URL. */ private function get_cache_url(): string { $upload_dir = wp_upload_dir(); return $upload_dir['baseurl'] . '/accessibility-audio-tts/cache/assets/'; } /** * Ensure cache directory exists. * * @since 1.0.0 * @return void */ private function ensure_cache_directory(): void { if ( ! file_exists( $this->cache_dir ) ) { wp_mkdir_p( $this->cache_dir ); // Add .htaccess for security. $htaccess_content = "# AAT Asset Cache\n"; $htaccess_content .= "\n"; $htaccess_content .= " Require all denied\n"; $htaccess_content .= "\n"; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- local cache write intended. file_put_contents( $this->cache_dir . '.htaccess', $htaccess_content ); } } /** * Clear minified assets cache. * * @since 1.0.0 * @return bool True on success. */ public function clear_minified_cache(): bool { if ( ! is_dir( $this->cache_dir ) ) { return true; } $files = glob( $this->cache_dir . 'accessibility-audio-tts-audio-*.min.*' ); $success = true; foreach ( $files as $file ) { if ( ! wp_delete_file( $file ) ) { $success = false; } } return $success; } /** * Get optimization statistics * * @since 1.0.0 * @return array Optimization stats */ public function get_optimization_stats(): array { return array_merge( $this->stats, array( 'cache_size' => $this->get_cache_size(), 'cached_files' => $this->get_cached_files_count(), ) ); } /** * Get cache directory size * * @since 1.0.0 * @return int Size in bytes */ private function get_cache_size(): int { if ( ! is_dir( $this->cache_dir ) ) { return 0; } $size = 0; $files = glob( $this->cache_dir . '*' ); foreach ( $files as $file ) { if ( is_file( $file ) ) { $size += filesize( $file ); } } return $size; } /** * Get number of cached files * * @since 1.0.0 * @return int Number of cached files */ private function get_cached_files_count(): int { if ( ! is_dir( $this->cache_dir ) ) { return 0; } $files = glob( $this->cache_dir . 'accessibility-audio-tts-audio-*.min.*' ); return count( $files ); } /** * Check if asset should be optimized * * @since 1.0.0 * @param string $src Asset source. * @return bool True if should optimize. */ public function should_optimize_asset( string $src ): bool { // Don't optimize already minified files. if ( false !== strpos( $src, '.min.' ) ) { return false; } // Don't optimize external assets (unless specifically allowed). if ( 0 !== strpos( $src, home_url() ) && 0 !== strpos( $src, ACCESSIBILITY_AUDIO_TTS_PLUGIN_URL ) ) { return false; } // Check file size - don't optimize very small files. $content = $this->get_asset_content( $src ); if ( $content && strlen( $content ) < 1024 ) { // Less than 1KB. return false; } return true; } }