settings = $this->get_settings(); add_action('admin_menu', [$this, 'add_admin_menu']); add_action('admin_init', [$this, 'register_settings']); add_action('admin_init', [$this, 'handle_actions']); add_action('admin_bar_menu', [$this, 'add_admin_bar_menu'], 100); add_filter('plugin_action_links_' . plugin_basename(__FILE__), [$this, 'add_action_links']); add_action('admin_enqueue_scripts', [$this, 'admin_styles']); // Initialize optimization features $this->init_optimizations(); // Initialize plugin compatibility hooks for cache invalidation $this->init_plugin_compatibility(); // Initialize smart page cache purge hooks $this->init_page_cache_purge_hooks(); } /** * Initialize hooks for smart page cache purging * Automatically purges nginx cache when content changes */ private function init_page_cache_purge_hooks() { // Only initialize if page cache is enabled if (!$this->settings['page_cache']) { return; } // Post changes add_action('save_post', [$this, 'purge_post_cache'], 10, 3); add_action('wp_trash_post', [$this, 'purge_post_cache_on_delete']); add_action('delete_post', [$this, 'purge_post_cache_on_delete']); add_action('edit_post', [$this, 'purge_post_cache_on_edit'], 10, 2); // Comment changes add_action('comment_post', [$this, 'purge_comment_cache'], 10, 2); add_action('edit_comment', [$this, 'purge_comment_cache']); add_action('wp_set_comment_status', [$this, 'purge_comment_cache']); // Menu changes add_action('wp_update_nav_menu', [$this, 'purge_all_cache']); // Theme/Customizer changes add_action('switch_theme', [$this, 'purge_all_cache']); add_action('customize_save_after', [$this, 'purge_all_cache']); add_action('update_option_theme_mods_' . get_template(), [$this, 'purge_all_cache']); // Sidebar/Widget changes add_action('update_option_sidebars_widgets', [$this, 'purge_all_cache']); // Permalink structure changes add_action('update_option_permalink_structure', [$this, 'purge_all_cache']); // WooCommerce specific hooks if (class_exists('WooCommerce')) { add_action('woocommerce_product_set_stock', [$this, 'purge_woocommerce_product']); add_action('woocommerce_variation_set_stock', [$this, 'purge_woocommerce_product']); add_action('woocommerce_product_set_stock_status', [$this, 'purge_woocommerce_product_stock_status'], 10, 3); add_action('woocommerce_update_product', [$this, 'purge_woocommerce_product_by_id']); add_action('woocommerce_new_product', [$this, 'purge_woocommerce_product_by_id']); add_action('woocommerce_delete_product', [$this, 'purge_woocommerce_product_by_id']); add_action('woocommerce_trash_product', [$this, 'purge_woocommerce_product_by_id']); } } /** * Purge cache for a specific post and related pages */ public function purge_post_cache($post_id, $post = null, $update = true) { // Skip autosaves and revisions if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { return; } if (!$post) { $post = get_post($post_id); } if (!$post || $post->post_status === 'auto-draft') { return; } $paths_to_purge = $this->get_post_related_paths($post); $this->purge_paths($paths_to_purge); } /** * Purge cache when post is deleted or trashed */ public function purge_post_cache_on_delete($post_id) { $post = get_post($post_id); if (!$post) { return; } $paths_to_purge = $this->get_post_related_paths($post); $this->purge_paths($paths_to_purge); } /** * Purge cache on post edit */ public function purge_post_cache_on_edit($post_id, $post) { $this->purge_post_cache($post_id, $post, true); } /** * Purge cache when a comment changes */ public function purge_comment_cache($comment_id, $approved = null) { $comment = get_comment($comment_id); if (!$comment) { return; } $post = get_post($comment->comment_post_ID); if ($post) { $paths_to_purge = [get_permalink($post)]; $this->purge_paths($paths_to_purge); } } /** * Get all paths related to a post that should be purged */ private function get_post_related_paths($post) { $paths = []; // Post URL $permalink = get_permalink($post); if ($permalink) { $paths[] = $permalink; } // Home page $paths[] = home_url('/'); // Blog page (if different from home) $blog_page_id = get_option('page_for_posts'); if ($blog_page_id) { $paths[] = get_permalink($blog_page_id); } // Category archives $categories = get_the_category($post->ID); if ($categories) { foreach ($categories as $category) { $paths[] = get_category_link($category->term_id); } } // Tag archives $tags = get_the_tags($post->ID); if ($tags) { foreach ($tags as $tag) { $paths[] = get_tag_link($tag->term_id); } } // Author archive $paths[] = get_author_posts_url($post->post_author); // Date archives $post_date = strtotime($post->post_date); $paths[] = get_year_link(date('Y', $post_date)); $paths[] = get_month_link(date('Y', $post_date), date('m', $post_date)); $paths[] = get_day_link(date('Y', $post_date), date('m', $post_date), date('d', $post_date)); // Post type archive (for custom post types) if ($post->post_type !== 'post' && $post->post_type !== 'page') { $archive_link = get_post_type_archive_link($post->post_type); if ($archive_link) { $paths[] = $archive_link; } } // Taxonomy archives for custom post types $taxonomies = get_object_taxonomies($post->post_type); foreach ($taxonomies as $taxonomy) { if ($taxonomy !== 'category' && $taxonomy !== 'post_tag') { $terms = get_the_terms($post->ID, $taxonomy); if ($terms && !is_wp_error($terms)) { foreach ($terms as $term) { $paths[] = get_term_link($term); } } } } // Remove duplicates and filter $paths = array_unique(array_filter($paths, function($path) { return !is_wp_error($path) && !empty($path); })); return $paths; } /** * Purge cache for WooCommerce product (from stock change) */ public function purge_woocommerce_product($product) { if (is_numeric($product)) { $product_id = $product; } else { $product_id = $product->get_id(); } $this->purge_woocommerce_product_by_id($product_id); } /** * Purge cache for WooCommerce product stock status change */ public function purge_woocommerce_product_stock_status($product_id, $stock_status, $product) { $this->purge_woocommerce_product_by_id($product_id); } /** * Purge cache for WooCommerce product by ID */ public function purge_woocommerce_product_by_id($product_id) { $paths = []; // Product URL $permalink = get_permalink($product_id); if ($permalink) { $paths[] = $permalink; } // Home page $paths[] = home_url('/'); // Shop page $shop_page_id = wc_get_page_id('shop'); if ($shop_page_id > 0) { $paths[] = get_permalink($shop_page_id); } // Product categories $terms = get_the_terms($product_id, 'product_cat'); if ($terms && !is_wp_error($terms)) { foreach ($terms as $term) { $paths[] = get_term_link($term); } } // Product tags $tags = get_the_terms($product_id, 'product_tag'); if ($tags && !is_wp_error($tags)) { foreach ($tags as $tag) { $paths[] = get_term_link($tag); } } $paths = array_unique(array_filter($paths, function($path) { return !is_wp_error($path) && !empty($path); })); $this->purge_paths($paths); } /** * Purge all cache (for site-wide changes) */ public function purge_all_cache() { $this->purge_paths(['/']); } /** * Send purge request to Jabali Panel API */ private function purge_paths(array $paths) { if (empty($paths)) { return false; } // Get domain from site URL $site_url = get_site_url(); $parsed = parse_url($site_url); $domain = $parsed['host'] ?? ''; if (empty($domain)) { return false; } // Generate secret from AUTH_KEY $secret = defined('AUTH_KEY') ? substr(md5(AUTH_KEY), 0, 32) : ''; if (empty($secret)) { return false; } // Convert full URLs to paths $normalized_paths = []; foreach ($paths as $url) { if ($url === '/') { $normalized_paths[] = '/'; } else { $parsed_url = parse_url($url); $path = $parsed_url['path'] ?? '/'; $normalized_paths[] = $path; } } $normalized_paths = array_unique($normalized_paths); // Check if we should purge all (if home is in the list) $purge_all = in_array('/', $normalized_paths) && count($normalized_paths) > 5; // Call Jabali Panel internal API $response = wp_remote_post('https://127.0.0.1/api/internal/page-cache-purge', [ 'timeout' => 10, 'sslverify' => false, 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'body' => json_encode([ 'domain' => $domain, 'paths' => $purge_all ? [] : $normalized_paths, 'purge_all' => $purge_all, 'secret' => $secret, ]), ]); if (is_wp_error($response)) { if (defined('WP_DEBUG') && WP_DEBUG) { error_log('Jabali Cache: Failed to purge page cache - ' . $response->get_error_message()); } return false; } $code = wp_remote_retrieve_response_code($response); if ($code !== 200) { if (defined('WP_DEBUG') && WP_DEBUG) { $body = wp_remote_retrieve_body($response); error_log('Jabali Cache: Failed to purge page cache - HTTP ' . $code . ' - ' . $body); } return false; } if (defined('WP_DEBUG') && WP_DEBUG) { error_log('Jabali Cache: Purged ' . count($normalized_paths) . ' paths'); } return true; } /** * Initialize hooks for theme/plugin asset regeneration * Uses general WordPress hooks instead of plugin-specific ones */ private function init_plugin_compatibility() { // General: Catch ALL option updates that might trigger CSS/JS regeneration add_action('updated_option', [$this, 'on_option_updated'], 10, 3); // General: Theme/plugin updates add_action('upgrader_process_complete', [$this, 'flush_all_builder_cache'], 10, 2); add_action('switch_theme', [$this, 'flush_all_builder_cache']); add_action('customize_save_after', [$this, 'flush_all_builder_cache']); // General: When any cache clear action is triggered by popular builders // These are catch-all hooks that most builders fire add_action('wp_cache_flush', [$this, 'flush_all_builder_cache']); add_action('litespeed_purge_all', [$this, 'flush_all_builder_cache']); // WooCommerce product changes (affects displayed content) add_action('woocommerce_delete_product_transients', [$this, 'flush_woocommerce_cache']); add_action('woocommerce_clear_product_transients', [$this, 'flush_woocommerce_cache']); } /** * Patterns in option names that indicate CSS/JS regeneration needed */ private function get_builder_option_patterns(): array { return [ // Page builders 'elementor', 'uagb', 'spectra', 'fl_builder', 'beaver', 'et_divi', 'et_builder', 'vc_', 'wpb_', 'generateblocks', 'kadence', 'brizy', 'oxygen', 'breakdance', // Themes 'astra', 'theme_mods_', 'flavor', // Optimization plugins 'autoptimize', 'wp_rocket', 'sg_optimizer', 'litespeed', 'w3tc', 'wp_optimize', // CSS/JS settings '_css', '_js', '_style', '_script', '_assets', '_cache', ]; } /** * Check if option update should trigger cache flush */ public function on_option_updated($option, $old_value, $new_value) { // Skip if values are identical if ($old_value === $new_value) { return; } $option_lower = strtolower($option); foreach ($this->get_builder_option_patterns() as $pattern) { if (strpos($option_lower, $pattern) !== false) { $this->flush_all_builder_cache(); return; } } } /** * Flush ALL builder-related transients and page cache * Single method that handles all builders/themes */ public function flush_all_builder_cache($upgrader = null, $options = null) { // Prevent multiple flushes in same request static $flushed = false; if ($flushed) { return; } $flushed = true; // Flush Redis transients matching any builder pattern $patterns = [ // Page builders 'elementor_', '_elementor', 'uagb_', '_uagb', 'spectra_', '_spectra', 'fl_builder_', '_fl_builder', 'et_core_', 'et_builder_', '_et_core', '_et_builder', 'vc_', '_vc_', 'generateblocks_', 'kadence_', 'kt_', 'brizy_', 'oxygen_', 'breakdance_', // Themes 'astra_', '_astra', 'theme_mod_', // Optimization 'autoptimize_', 'wprocket_', 'litespeed_', // Generic '_css_', '_js_', '_style_', '_assets_', ]; $this->flush_transients_by_patterns($patterns); // Purge nginx page cache if ($this->settings['page_cache']) { $this->purge_all_cache(); } $this->log_cache_flush('Builder/Theme'); } /** * Flush WooCommerce related transients */ public function flush_woocommerce_cache() { $this->flush_transients_by_patterns(['wc_', '_wc_', 'woocommerce_']); $this->log_cache_flush('WooCommerce'); } /** * Flush transients matching specific patterns from Redis * * @param array $patterns Array of patterns to match (e.g., ['uagb_', 'spectra_']) * @return int Number of keys deleted */ private function flush_transients_by_patterns(array $patterns) { if (!$this->is_redis_connected()) { return 0; } try { $redis = new Redis(); $host = defined('JABALI_CACHE_HOST') ? JABALI_CACHE_HOST : '127.0.0.1'; $port = defined('JABALI_CACHE_PORT') ? JABALI_CACHE_PORT : 6379; $db = defined('JABALI_CACHE_DATABASE') ? JABALI_CACHE_DATABASE : 0; $prefix = defined('JABALI_CACHE_PREFIX') ? JABALI_CACHE_PREFIX : ''; if (!$redis->connect($host, $port, 2)) { return 0; } // Authenticate if credentials are defined if (defined('JABALI_CACHE_REDIS_USER') && defined('JABALI_CACHE_REDIS_PASS')) { $redis->auth(['user' => JABALI_CACHE_REDIS_USER, 'pass' => JABALI_CACHE_REDIS_PASS]); } elseif (defined('JABALI_CACHE_REDIS_PASS') && JABALI_CACHE_REDIS_PASS) { $redis->auth(JABALI_CACHE_REDIS_PASS); } $redis->select($db); $deleted = 0; foreach ($patterns as $pattern) { // Search for transient keys matching the pattern // WordPress stores transients as: {prefix}transient_{name} and {prefix}_transient_{name} $scan_patterns = [ $prefix . '*transient*' . $pattern . '*', $prefix . '*' . $pattern . '*transient*', $prefix . '*' . $pattern . '*', ]; foreach ($scan_patterns as $scan_pattern) { $cursor = null; do { $keys = $redis->scan($cursor, $scan_pattern, 100); if ($keys !== false && !empty($keys)) { foreach ($keys as $key) { $redis->del($key); $deleted++; } } } while ($cursor > 0); } } $redis->close(); return $deleted; } catch (Exception $e) { error_log('Jabali Cache: Failed to flush transients - ' . $e->getMessage()); return 0; } } /** * Log cache flush event for debugging */ private function log_cache_flush($plugin_name) { if (defined('WP_DEBUG') && WP_DEBUG) { error_log(sprintf('Jabali Cache: Flushed Redis transients triggered by %s asset regeneration', $plugin_name)); } } public function get_settings() { $defaults = [ 'page_cache' => true, 'object_cache' => true, 'html_minify' => false, 'minify_css' => true, 'minify_js' => true, 'lazy_load' => false, 'lazy_load_iframes' => false, 'browser_cache' => false, 'expired_cache' => false, 'remove_query_strings' => false, 'disable_emojis' => false, 'disable_embeds' => false, 'defer_js' => false, 'cache_debug' => true, 'local_google_fonts' => false, // LCP Optimization settings 'lcp_preload' => true, 'preconnect_hints' => true, 'preload_fonts' => true, 'delay_third_party' => false, ]; $saved = get_option(self::OPTION_KEY, []); return array_merge($defaults, $saved); } public function init_optimizations() { // Page Cache bypass - send no-cache headers when disabled if (!$this->settings['page_cache'] && !is_admin()) { add_action('send_headers', [$this, 'disable_page_cache_headers'], 1); } // HTML Minification if ($this->settings['html_minify'] && !is_admin()) { add_action('template_redirect', [$this, 'start_html_buffer'], 1); } // CSS Minification if ($this->settings['minify_css'] && !is_admin()) { add_filter('style_loader_tag', [$this, 'minify_inline_css'], 10, 4); add_action('wp_head', [$this, 'start_css_capture'], 1); add_action('wp_head', [$this, 'end_css_capture'], 999); } // JS Minification if ($this->settings['minify_js'] && !is_admin()) { add_filter('script_loader_tag', [$this, 'minify_inline_js'], 10, 3); } // Defer JS if ($this->settings['defer_js'] && !is_admin()) { add_filter('script_loader_tag', [$this, 'defer_js'], 10, 3); } // Lazy Loading if ($this->settings['lazy_load'] && !is_admin()) { add_filter('the_content', [$this, 'add_lazy_loading']); add_filter('post_thumbnail_html', [$this, 'add_lazy_loading']); add_filter('get_avatar', [$this, 'add_lazy_loading']); add_filter('widget_text', [$this, 'add_lazy_loading']); } // Lazy Load iframes if ($this->settings['lazy_load_iframes'] && !is_admin()) { add_filter('the_content', [$this, 'add_lazy_loading_iframes']); add_filter('embed_oembed_html', [$this, 'add_lazy_loading_iframes']); } // Remove Query Strings if ($this->settings['remove_query_strings'] && !is_admin()) { add_filter('script_loader_src', [$this, 'remove_query_strings'], 15); add_filter('style_loader_src', [$this, 'remove_query_strings'], 15); } // Disable Emojis if ($this->settings['disable_emojis']) { $this->disable_emojis(); } // Disable Embeds if ($this->settings['disable_embeds']) { $this->disable_embeds(); } // Expired Cache (No-Cache Headers) if ($this->settings['expired_cache'] && !is_admin()) { add_action('template_redirect', [$this, 'send_expired_cache_headers'], 1); add_action('send_headers', [$this, 'send_expired_cache_headers']); } // Debug mode - add debug info for admins if ($this->settings['cache_debug'] && !is_admin()) { add_action('wp_footer', [$this, 'output_debug_info'], 999); } // Local Google Fonts if ($this->settings['local_google_fonts'] && !is_admin()) { add_filter('style_loader_src', [$this, 'localize_google_fonts_css'], 10, 2); add_filter('wp_resource_hints', [$this, 'remove_google_fonts_preconnect'], 10, 2); } // LCP Image Optimization (preload + fetchpriority) if ($this->settings['lcp_preload'] && !is_admin()) { add_filter('the_content', [$this, 'optimize_lcp_image'], 5); add_filter('post_thumbnail_html', [$this, 'optimize_lcp_image'], 5); add_action('wp_head', [$this, 'output_lcp_preload'], 1); } // Preconnect Hints if ($this->settings['preconnect_hints'] && !is_admin()) { add_action('wp_head', [$this, 'output_preconnect_hints'], 1); } // Font Preload if ($this->settings['preload_fonts'] && !is_admin()) { add_action('wp_head', [$this, 'output_font_preload'], 2); } // Delay Third-Party Scripts if ($this->settings['delay_third_party'] && !is_admin()) { add_filter('script_loader_tag', [$this, 'delay_third_party_scripts'], 10, 3); } } /** * CSS Minification - minify inline styles in style tags */ public function minify_inline_css($html, $handle, $href, $media) { return $html; // Return unchanged, we don't minify external CSS files } private $css_buffer_active = false; public function start_css_capture() { // Not implemented - inline CSS minification would require output buffering // which conflicts with page cache. External files are better cached by nginx. } public function end_css_capture() { // Not implemented } /** * Minify inline JS within script tags */ public function minify_inline_js($tag, $handle, $src) { // Don't touch external scripts if (!empty($src)) { return $tag; } // Skip certain handles that shouldn't be minified $skip_handles = ['jquery-core', 'jquery-migrate', 'wp-embed']; if (in_array($handle, $skip_handles)) { return $tag; } // Simple minification for inline scripts - just collapse whitespace // Being conservative to avoid breaking scripts $tag = preg_replace_callback('/]*)>(.*?)<\/script>/is', function($match) { $attrs = $match[1]; $content = $match[2]; // Skip if it contains JSON or data if (strpos($content, 'application/json') !== false || strpos($content, 'application/ld+json') !== false) { return $match[0]; } // Very conservative minification - only collapse multiple spaces/newlines $content = preg_replace('/\s{2,}/', ' ', $content); $content = trim($content); return '' . $content . ''; }, $tag); return $tag; } /** * Output debug info for admins */ public function output_debug_info() { // Only show for admins with cache_debug param or setting enabled if (!current_user_can('manage_options')) { return; } // Check if ?cache_debug=1 is set if (!isset($_GET['cache_debug']) && !$this->settings['cache_debug']) { return; } // Only output if explicitly requested via query param if (!isset($_GET['cache_debug']) || $_GET['cache_debug'] !== '1') { return; } $debug_info = []; $debug_info['plugin_version'] = self::VERSION; $debug_info['page_cache'] = $this->settings['page_cache'] ? 'enabled' : 'disabled'; $debug_info['object_cache'] = $this->is_drop_in_installed() ? 'enabled' : 'disabled'; $debug_info['redis_connected'] = $this->is_redis_connected() ? 'yes' : 'no'; $debug_info['minify_css'] = $this->settings['minify_css'] ? 'enabled' : 'disabled'; $debug_info['minify_js'] = $this->settings['minify_js'] ? 'enabled' : 'disabled'; $debug_info['defer_js'] = $this->settings['defer_js'] ? 'enabled' : 'disabled'; $debug_info['lazy_load'] = $this->settings['lazy_load'] ? 'enabled' : 'disabled'; $debug_info['logged_in'] = is_user_logged_in() ? 'yes' : 'no'; $debug_info['bypass_reason'] = $this->get_cache_bypass_reason(); $debug_info['page_generated'] = date('Y-m-d H:i:s'); $debug_info['queries'] = get_num_queries(); $debug_info['memory_peak'] = size_format(memory_get_peak_usage(true)); echo "\n\n"; } /** * Get reason why page cache might be bypassed */ private function get_cache_bypass_reason() { if (is_user_logged_in()) { return 'logged_in_user'; } if (isset($_POST) && !empty($_POST)) { return 'post_request'; } if (is_admin()) { return 'admin_page'; } // Check for WooCommerce cart/checkout if (function_exists('is_cart') && is_cart()) { return 'woocommerce_cart'; } if (function_exists('is_checkout') && is_checkout()) { return 'woocommerce_checkout'; } if (function_exists('is_account_page') && is_account_page()) { return 'woocommerce_account'; } // Check for cookies that trigger bypass if (isset($_COOKIE['wordpress_logged_in']) || isset($_COOKIE['wp-postpass'])) { return 'bypass_cookie'; } if (isset($_COOKIE['woocommerce_cart_hash']) || isset($_COOKIE['woocommerce_items_in_cart'])) { return 'woocommerce_cookie'; } return 'none'; } // HTML Minification public function start_html_buffer() { ob_start([$this, 'minify_html']); } public function minify_html($html) { if (empty($html)) { return $html; } // Don't minify feeds, sitemaps, or admin if (is_feed() || is_admin()) { return $html; } // Preserve scripts and styles $preserved = []; $html = preg_replace_callback('/<(script|style|pre|textarea)[^>]*>.*?<\/\1>/is', function($match) use (&$preserved) { $key = ''; $preserved[$key] = $match[0]; return $key; }, $html); // Remove HTML comments (except IE conditionals and PRESERVE markers) $html = preg_replace('//s', '', $html); // Remove whitespace between tags $html = preg_replace('/>\s+<', $html); // Remove extra whitespace $html = preg_replace('/\s{2,}/', ' ', $html); // Remove whitespace around block elements $html = preg_replace('/\s*(<\/?(?:html|head|body|div|section|article|header|footer|nav|aside|main|p|h[1-6]|ul|ol|li|table|tr|td|th|form|fieldset|dl|dt|dd|blockquote|hr|br)[^>]*>)\s*/i', '$1', $html); // Restore preserved content $html = str_replace(array_keys($preserved), array_values($preserved), $html); return trim($html); } // Lazy Loading public function add_lazy_loading($content) { if (empty($content)) { return $content; } // Add loading="lazy" to images that don't have it $content = preg_replace_callback('/]+)>/i', function($match) { $img = $match[0]; // Skip if already has loading attribute if (strpos($img, 'loading=') !== false) { return $img; } // Skip base64 images and SVGs if (strpos($img, 'data:image') !== false || strpos($img, '.svg') !== false) { return $img; } // Add loading="lazy" return str_replace(']+)>/i', function($match) { $iframe = $match[0]; if (strpos($iframe, 'loading=') !== false) { return $iframe; } return str_replace(' $rewrite) { if (strpos($rewrite, 'embed=true') !== false) { unset($rules[$rule]); } } return $rules; }); } // ========================================================================= // LCP OPTIMIZATION // ========================================================================= private $lcp_image_url = null; private $lcp_image_srcset = null; private $lcp_image_sizes = null; private $lcp_processed = false; /** * Detect and optimize LCP image * Adds fetchpriority="high" and loading="eager" to the first large image * Also excludes it from lazy loading */ public function optimize_lcp_image($content) { if (empty($content) || $this->lcp_processed) { return $content; } // Find all images in content preg_match_all('/]+>/i', $content, $matches); if (empty($matches[0])) { return $content; } $first_image = true; foreach ($matches[0] as $img) { // Skip small images (icons, avatars, etc.) if ($this->is_small_image($img)) { continue; } // Skip images that already have fetchpriority if (strpos($img, 'fetchpriority') !== false) { continue; } // Skip lazy-loaded images from other plugins if (strpos($img, 'data-src') !== false && strpos($img, ' src=') === false) { continue; } if ($first_image) { // This is our LCP candidate $optimized_img = $img; // Add fetchpriority="high" if (strpos($optimized_img, 'fetchpriority') === false) { $optimized_img = str_replace('lcp_image_url = $src_match[1]; } // Extract srcset for preload if (preg_match('/srcset=["\']([^"\']+)["\']/i', $optimized_img, $srcset_match)) { $this->lcp_image_srcset = $srcset_match[1]; } // Extract sizes for preload if (preg_match('/sizes=["\']([^"\']+)["\']/i', $optimized_img, $sizes_match)) { $this->lcp_image_sizes = $sizes_match[1]; } $content = str_replace($img, $optimized_img, $content); $this->lcp_processed = true; $first_image = false; } } return $content; } /** * Check if image is likely small (icon, avatar, etc.) */ private function is_small_image($img) { // Check for explicit small dimensions if (preg_match('/width=["\']?(\d+)/i', $img, $w_match)) { if ((int)$w_match[1] < 150) { return true; } } if (preg_match('/height=["\']?(\d+)/i', $img, $h_match)) { if ((int)$h_match[1] < 150) { return true; } } // Check for common small image classes $small_classes = ['avatar', 'icon', 'logo', 'emoji', 'thumbnail', 'wp-smiley']; foreach ($small_classes as $class) { if (stripos($img, $class) !== false) { return true; } } // Check for small image sizes in filename if (preg_match('/-(\d+)x(\d+)\./i', $img, $size_match)) { if ((int)$size_match[1] < 150 || (int)$size_match[2] < 150) { return true; } } return false; } /** * Output LCP image preload link in head */ public function output_lcp_preload() { // If we haven't processed content yet, try to get featured image if (!$this->lcp_image_url && is_singular()) { $post_id = get_the_ID(); if ($post_id && has_post_thumbnail($post_id)) { $thumbnail_id = get_post_thumbnail_id($post_id); $this->lcp_image_url = wp_get_attachment_image_url($thumbnail_id, 'large'); // Get srcset for responsive preload $this->lcp_image_srcset = wp_get_attachment_image_srcset($thumbnail_id, 'large'); $this->lcp_image_sizes = wp_get_attachment_image_sizes($thumbnail_id, 'large'); } } if (!$this->lcp_image_url) { return; } // Build preload link $preload = 'lcp_image_srcset) { $preload .= ' imagesrcset="' . esc_attr($this->lcp_image_srcset) . '"'; } if ($this->lcp_image_sizes) { $preload .= ' imagesizes="' . esc_attr($this->lcp_image_sizes) . '"'; } $preload .= ' fetchpriority="high">'; echo $preload . "\n"; } // ========================================================================= // PRECONNECT HINTS // ========================================================================= /** * Output preconnect hints for external domains */ public function output_preconnect_hints() { $domains = $this->get_preconnect_domains(); foreach ($domains as $domain) { echo '' . "\n"; } } /** * Get list of domains that should have preconnect hints */ private function get_preconnect_domains() { $domains = []; $site_host = parse_url(home_url(), PHP_URL_HOST); // Common CDN and font domains $common_domains = [ 'fonts.gstatic.com', 'cdnjs.cloudflare.com', 'cdn.jsdelivr.net', 'unpkg.com', 'ajax.googleapis.com', 'www.googletagmanager.com', 'www.google-analytics.com', 'connect.facebook.net', 'platform.twitter.com', ]; // Check if local Google Fonts is disabled (then we need preconnect for Google) if (!$this->settings['local_google_fonts']) { $domains[] = 'https://fonts.googleapis.com'; $domains[] = 'https://fonts.gstatic.com'; } // Check for common CDN usage in theme $template_uri = get_template_directory_uri(); $template_host = parse_url($template_uri, PHP_URL_HOST); if ($template_host && $template_host !== $site_host) { $domains[] = 'https://' . $template_host; } // Check WooCommerce if (class_exists('WooCommerce')) { // WooCommerce uses some external resources // Usually handled locally, but check for PayPal, Stripe, etc. } // Remove duplicates $domains = array_unique($domains); // Limit to 4 most important (too many preconnects hurt performance) return array_slice($domains, 0, 4); } // ========================================================================= // FONT PRELOAD // ========================================================================= /** * Preload critical fonts */ public function output_font_preload() { // If using local Google Fonts, preload the first cached font file if ($this->settings['local_google_fonts']) { $cache_dir = WP_CONTENT_DIR . '/cache/jabali/fonts'; $cache_url = content_url('/cache/jabali/fonts'); if (is_dir($cache_dir)) { $fonts = glob($cache_dir . '/*.woff2'); if (!empty($fonts)) { // Preload first 2 font files (usually regular and bold) $count = 0; foreach ($fonts as $font) { if ($count >= 2) break; $font_url = $cache_url . '/' . basename($font); echo '' . "\n"; $count++; } } } } } // ========================================================================= // IMPROVED DEFER JS WITH EXCLUSIONS // ========================================================================= /** * Default list of script handles that should NOT be deferred */ private function get_defer_js_exclusions() { return [ // Core jQuery - many inline scripts depend on it 'jquery-core', 'jquery-migrate', 'jquery', // WordPress core 'wp-polyfill', 'wp-hooks', // WooCommerce critical scripts 'wc-cart-fragments', 'wc-checkout', 'wc-add-to-cart', 'wc-single-product', 'woocommerce', // Elementor critical 'elementor-frontend', 'elementor-pro-frontend', // Spectra/UAG critical 'uagb-block-positioning-js', 'uagb-js', // Astra theme 'astra-theme-js', 'astra-addon-js', // Common sliders that need to run early 'swiper', 'slick', 'owl-carousel', // Critical UI libraries 'lodash', 'underscore', 'backbone', ]; } /** * Defer non-critical JavaScript with proper exclusions */ public function defer_js($tag, $handle, $src) { // Skip if no src (inline script) if (empty($src)) { return $tag; } // Get exclusion list $exclusions = $this->get_defer_js_exclusions(); // Allow filtering exclusions $exclusions = apply_filters('jabali_cache_defer_js_exclusions', $exclusions); // Don't defer excluded scripts if (in_array($handle, $exclusions)) { return $tag; } // Don't defer if already has defer or async if (strpos($tag, ' defer') !== false || strpos($tag, ' async') !== false) { return $tag; } // Don't defer scripts with dependencies that aren't deferred // (this is a simplified check) if (strpos($handle, 'jquery') !== false && strpos($handle, 'jquery-core') === false) { // jQuery-dependent scripts should check if jQuery is available // For safety, we let them through but they'll have defer } // Add defer attribute return str_replace(' src=', ' defer src=', $tag); } // ========================================================================= // DELAY THIRD-PARTY SCRIPTS // ========================================================================= /** * Third-party script domains that should be delayed */ private function get_third_party_domains() { return [ 'google-analytics.com', 'googletagmanager.com', 'facebook.net', 'connect.facebook.com', 'platform.twitter.com', 'snap.licdn.com', 'static.hotjar.com', 'script.hotjar.com', 'widget.intercom.io', 'js.intercomcdn.com', 'cdn.segment.com', 'js.hs-scripts.com', 'js.hs-analytics.net', 'cdn.heapanalytics.com', 'cdn.mxpnl.com', 'rum-static.pingdom.net', 'newrelic.com', 'nr-data.net', 'fullstory.com', 'clarity.ms', 'browser.sentry-cdn.com', 'cdn.onesignal.com', 'cdn.pushwoosh.com', 'cdn.tawk.to', 'embed.tawk.to', 'static.zdassets.com', 'widget.drift.com', 'js.driftt.com', 'beacon-v2.helpscout.net', 'app.chatwoot.com', ]; } /** * Delay third-party scripts until user interaction */ public function delay_third_party_scripts($tag, $handle, $src) { if (empty($src)) { return $tag; } $third_party_domains = $this->get_third_party_domains(); $is_third_party = false; foreach ($third_party_domains as $domain) { if (strpos($src, $domain) !== false) { $is_third_party = true; break; } } if (!$is_third_party) { return $tag; } // Convert to delayed loading // Replace src with data-src and add class for delayed loading $tag = str_replace(' src=', ' data-jabali-delay-src=', $tag); $tag = str_replace(' 15, 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', ]); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { return $src; // Fall back to original URL } $css = wp_remote_retrieve_body($response); // Parse and download font files $css = $this->download_and_replace_font_urls($css, $cache_dir, $cache_url); // Save the modified CSS file_put_contents($cached_file, $css); return $cache_url . '/' . $cache_key . '.css'; } /** * Download font files and replace URLs in CSS */ private function download_and_replace_font_urls($css, $cache_dir, $cache_url) { // Find all font URLs in the CSS preg_match_all('/url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/i', $css, $matches); if (empty($matches[1])) { return $css; } $font_urls = array_unique($matches[1]); foreach ($font_urls as $font_url) { // Generate local filename $font_hash = md5($font_url); $extension = pathinfo(parse_url($font_url, PHP_URL_PATH), PATHINFO_EXTENSION) ?: 'woff2'; $local_filename = $font_hash . '.' . $extension; $local_path = $cache_dir . '/' . $local_filename; $local_url = $cache_url . '/' . $local_filename; // Download font if not cached if (!file_exists($local_path)) { $font_response = wp_remote_get($font_url, [ 'timeout' => 30, ]); if (!is_wp_error($font_response) && wp_remote_retrieve_response_code($font_response) === 200) { file_put_contents($local_path, wp_remote_retrieve_body($font_response)); } else { continue; // Skip this font, keep original URL } } // Replace URL in CSS $css = str_replace($font_url, $local_url, $css); } return $css; } /** * Remove Google Fonts preconnect hints */ public function remove_google_fonts_preconnect($hints, $relation_type) { if ($relation_type === 'preconnect' || $relation_type === 'dns-prefetch') { $hints = array_filter($hints, function($hint) { $url = is_array($hint) ? ($hint['href'] ?? '') : $hint; return strpos($url, 'fonts.googleapis.com') === false && strpos($url, 'fonts.gstatic.com') === false; }); } return $hints; } /** * Get local Google Fonts cache stats */ public function get_local_fonts_stats() { $cache_dir = WP_CONTENT_DIR . '/cache/jabali/fonts'; $stats = [ 'enabled' => $this->settings['local_google_fonts'], 'cached_files' => 0, 'cache_size' => 0, ]; if (is_dir($cache_dir)) { $files = glob($cache_dir . '/*'); $stats['cached_files'] = count($files); foreach ($files as $file) { $stats['cache_size'] += filesize($file); } } return $stats; } // Expired Cache - Send no-cache headers public function send_expired_cache_headers() { // Skip if headers already sent if (headers_sent()) { return; } // Skip admin pages if (is_admin()) { return; } // Set headers to prevent caching header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0', true); header('Pragma: no-cache', true); header('Expires: Thu, 01 Jan 1970 00:00:00 GMT', true); header('X-Jabali-Cache: no-cache', true); } // Disable Page Cache - Send headers to bypass nginx fastcgi_cache public function disable_page_cache_headers() { // Skip if headers already sent if (headers_sent()) { return; } // Skip admin pages if (is_admin()) { return; } // Set headers to tell nginx to not cache this response // and to bypass the cache when serving header('Cache-Control: no-store, no-cache, must-revalidate, private, max-age=0', true); header('Pragma: no-cache', true); header('X-Jabali-Skip-Cache: 1', true); header('X-Accel-Expires: 0', true); // nginx-specific: don't cache } // Admin Menu public function add_admin_menu() { add_options_page( __('Jabali Cache', 'jabali-cache'), __('Jabali Cache', 'jabali-cache'), 'manage_options', 'jabali-cache', [$this, 'render_admin_page'] ); } public function register_settings() { register_setting(self::OPTION_KEY, self::OPTION_KEY, [ 'sanitize_callback' => [$this, 'sanitize_settings'] ]); } public function sanitize_settings($input) { $sanitized = []; $checkboxes = [ 'page_cache', 'object_cache', 'html_minify', 'minify_css', 'minify_js', 'lazy_load', 'lazy_load_iframes', 'browser_cache', 'expired_cache', 'remove_query_strings', 'disable_emojis', 'disable_embeds', 'defer_js', 'cache_debug', 'local_google_fonts', // LCP Optimization settings 'lcp_preload', 'preconnect_hints', 'preload_fonts', 'delay_third_party' ]; foreach ($checkboxes as $key) { $sanitized[$key] = !empty($input[$key]); } // Sync page cache with Jabali Panel (nginx FastCGI cache) $old_settings = $this->get_settings(); if ($sanitized['page_cache'] !== $old_settings['page_cache']) { $this->sync_page_cache_with_jabali($sanitized['page_cache']); } // Clear object cache for this option to ensure fresh read on next load wp_cache_delete(self::OPTION_KEY, 'options'); wp_cache_delete('alloptions', 'options'); return $sanitized; } /** * Sync page cache setting with Jabali Panel * This enables/disables nginx FastCGI cache for this domain */ private function sync_page_cache_with_jabali($enabled) { // Get domain from site URL $site_url = get_site_url(); $parsed = parse_url($site_url); $domain = $parsed['host'] ?? ''; if (empty($domain)) { return false; } // Generate secret from AUTH_KEY $secret = defined('AUTH_KEY') ? substr(md5(AUTH_KEY), 0, 32) : ''; if (empty($secret)) { return false; } // Call Jabali Panel internal API (HTTPS with SSL verify disabled for localhost) $response = wp_remote_post('https://127.0.0.1/api/internal/page-cache', [ 'timeout' => 10, 'sslverify' => false, 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'body' => json_encode([ 'domain' => $domain, 'enabled' => $enabled, 'secret' => $secret, ]), ]); if (is_wp_error($response)) { error_log('Jabali Cache: Failed to sync page cache - ' . $response->get_error_message()); return false; } $code = wp_remote_retrieve_response_code($response); if ($code !== 200) { $body = wp_remote_retrieve_body($response); error_log('Jabali Cache: Failed to sync page cache - HTTP ' . $code . ' - ' . $body); return false; } return true; } /** * Disable page cache when plugin is deactivated */ public function disable_page_cache_on_deactivation() { return $this->sync_page_cache_with_jabali(false); } /** * Enable page cache when plugin is activated (if enabled in settings) */ public function enable_page_cache_on_activation() { return $this->sync_page_cache_with_jabali(true); } public function admin_styles($hook) { if ($hook !== 'settings_page_jabali-cache') { return; } ?> is_redis_connected(); $wp_admin_bar->add_node([ 'id' => 'jabali-cache', 'title' => sprintf( ' %s', $connected ? '#46b450' : '#dc3232', __('Jabali Cache', 'jabali-cache') ), 'href' => admin_url('options-general.php?page=jabali-cache'), ]); $wp_admin_bar->add_node([ 'parent' => 'jabali-cache', 'id' => 'jabali-cache-clear-all', 'title' => __('Clear All Caches', 'jabali-cache'), 'href' => wp_nonce_url( admin_url('options-general.php?page=jabali-cache&action=clear_all'), 'jabali_cache_clear_all' ), ]); $wp_admin_bar->add_node([ 'parent' => 'jabali-cache', 'id' => 'jabali-cache-flush', 'title' => __('Flush Object Cache', 'jabali-cache'), 'href' => wp_nonce_url( admin_url('options-general.php?page=jabali-cache&action=flush'), 'jabali_cache_flush' ), ]); $wp_admin_bar->add_node([ 'parent' => 'jabali-cache', 'id' => 'jabali-cache-purge-page', 'title' => __('Purge Page Cache', 'jabali-cache'), 'href' => wp_nonce_url( admin_url('options-general.php?page=jabali-cache&action=purge_page'), 'jabali_cache_purge_page' ), ]); } public function add_action_links($links) { $settings_link = sprintf( '%s', admin_url('options-general.php?page=jabali-cache'), __('Settings', 'jabali-cache') ); array_unshift($links, $settings_link); return $links; } public function handle_actions() { if (!isset($_GET['page']) || $_GET['page'] !== 'jabali-cache' || !isset($_GET['action'])) { return; } $action = sanitize_text_field($_GET['action']); switch ($action) { case 'flush': if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'jabali_cache_flush')) { wp_die(__('Security check failed', 'jabali-cache')); } wp_cache_flush(); wp_redirect(admin_url('options-general.php?page=jabali-cache&flushed=1')); exit; case 'enable': if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'jabali_cache_enable')) { wp_die(__('Security check failed', 'jabali-cache')); } $this->enable_drop_in(); wp_redirect(admin_url('options-general.php?page=jabali-cache&enabled=1')); exit; case 'disable': if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'jabali_cache_disable')) { wp_die(__('Security check failed', 'jabali-cache')); } $this->disable_drop_in(); wp_redirect(admin_url('options-general.php?page=jabali-cache&disabled=1')); exit; case 'clear_all': if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'jabali_cache_clear_all')) { wp_die(__('Security check failed', 'jabali-cache')); } $this->clear_all_caches(); wp_redirect(admin_url('options-general.php?page=jabali-cache&cleared=1')); exit; case 'purge_page': if (!wp_verify_nonce($_GET['_wpnonce'] ?? '', 'jabali_cache_purge_page')) { wp_die(__('Security check failed', 'jabali-cache')); } $this->purge_all_cache(); wp_redirect(admin_url('options-general.php?page=jabali-cache&purged=1')); exit; } } /** * Clear all caches - combined CSS/JS files and object cache */ public function clear_all_caches() { // Clear combined CSS/JS cache files $cache_dir = WP_CONTENT_DIR . '/cache/jabali'; if (is_dir($cache_dir)) { $files = glob($cache_dir . '/*'); foreach ($files as $file) { if (is_file($file)) { @unlink($file); } } } // Flush object cache (Redis) if (function_exists('wp_cache_flush')) { wp_cache_flush(); } // Purge page cache $this->purge_all_cache(); } public function is_drop_in_installed() { $drop_in = WP_CONTENT_DIR . '/object-cache.php'; if (!file_exists($drop_in)) { return false; } $content = file_get_contents($drop_in); return strpos($content, 'Jabali_Redis_Object_Cache') !== false; } public function enable_drop_in() { $source = plugin_dir_path(__FILE__) . 'object-cache.php'; $dest = WP_CONTENT_DIR . '/object-cache.php'; if (file_exists($source)) { @copy($source, $dest); } } public function disable_drop_in() { $drop_in = WP_CONTENT_DIR . '/object-cache.php'; if (file_exists($drop_in)) { @unlink($drop_in); } } public function is_redis_connected() { if (!class_exists('Redis')) { return false; } try { $redis = new Redis(); $host = defined('JABALI_CACHE_HOST') ? JABALI_CACHE_HOST : '127.0.0.1'; $port = defined('JABALI_CACHE_PORT') ? JABALI_CACHE_PORT : 6379; $db = defined('JABALI_CACHE_DATABASE') ? JABALI_CACHE_DATABASE : 0; if (!$redis->connect($host, $port, 1)) { return false; } // Authenticate if credentials are defined if (defined('JABALI_CACHE_REDIS_USER') && defined('JABALI_CACHE_REDIS_PASS')) { $redis->auth(['user' => JABALI_CACHE_REDIS_USER, 'pass' => JABALI_CACHE_REDIS_PASS]); } elseif (defined('JABALI_CACHE_REDIS_PASS') && JABALI_CACHE_REDIS_PASS) { $redis->auth(JABALI_CACHE_REDIS_PASS); } // Select database $redis->select($db); return $redis->ping() ? true : false; } catch (Exception $e) { return false; } } public function get_cache_stats() { global $wp_object_cache; $stats = [ 'hits' => 0, 'misses' => 0, 'ratio' => 0, 'connected' => false, 'total_keys' => 0, 'memory' => 'N/A', ]; if (method_exists($wp_object_cache, 'stats')) { $cache_stats = $wp_object_cache->stats(); if (is_array($cache_stats)) { $stats = array_merge($stats, $cache_stats); } } elseif (isset($wp_object_cache->cache_hits)) { $stats['hits'] = $wp_object_cache->cache_hits; $stats['misses'] = $wp_object_cache->cache_misses ?? 0; } // Calculate ratio $total = $stats['hits'] + $stats['misses']; if ($total > 0) { $stats['ratio'] = round(($stats['hits'] / $total) * 100, 1); } return $stats; } public function render_admin_page() { $stats = $this->get_cache_stats(); $drop_in_installed = $this->is_drop_in_installed(); $redis_available = class_exists('Redis'); $redis_connected = $this->is_redis_connected(); ?>

%
100): ?>

enable_drop_in(); } // Always enable nginx page cache when plugin is activated // This is the main purpose of the plugin - users activate it because they want caching $plugin->enable_page_cache_on_activation(); // Initialize default settings if they don't exist $settings = get_option(Jabali_Cache_Plugin::OPTION_KEY); if ($settings === false) { update_option(Jabali_Cache_Plugin::OPTION_KEY, ['page_cache' => true]); } }); // Deactivation - disable object cache AND page cache register_deactivation_hook(__FILE__, function() { $plugin = Jabali_Cache_Plugin::get_instance(); $plugin->disable_drop_in(); // Also disable nginx page cache when plugin is deactivated $plugin->disable_page_cache_on_deactivation(); });