Files
2026-02-02 03:11:45 +02:00

2533 lines
103 KiB
PHP

<?php
/**
* Plugin Name: Jabali Cache
* Plugin URI: https://github.com/shukiv/jabali-panel
* Description: High-performance caching for WordPress - Redis object cache, nginx page cache, lazy loading, and more.
* Version: 2.6.0
* Author: Jabali Panel
* Author URI: https://github.com/shukiv/jabali-panel
* License: GPL-2.0+
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: jabali-cache
*
* @package Jabali_Cache
*/
defined('ABSPATH') || exit;
class Jabali_Cache_Plugin {
const VERSION = '2.6.0';
const OPTION_KEY = 'jabali_cache_settings';
private static $instance = null;
private $settings = [];
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
$this->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([^>]*)>(.*?)<\/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 '<script' . $attrs . '>' . $content . '</script>';
}, $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<!-- Jabali Cache Debug Info\n";
foreach ($debug_info as $key => $value) {
echo " {$key}: {$value}\n";
}
echo "-->\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 = '<!--PRESERVE:' . count($preserved) . '-->';
$preserved[$key] = $match[0];
return $key;
}, $html);
// Remove HTML comments (except IE conditionals and PRESERVE markers)
$html = preg_replace('/<!--(?!\[if|PRESERVE:).*?-->/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('/<img([^>]+)>/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('<img', '<img loading="lazy"', $img);
}, $content);
return $content;
}
public function add_lazy_loading_iframes($content) {
if (empty($content)) {
return $content;
}
// Add loading="lazy" to iframes
$content = preg_replace_callback('/<iframe([^>]+)>/i', function($match) {
$iframe = $match[0];
if (strpos($iframe, 'loading=') !== false) {
return $iframe;
}
return str_replace('<iframe', '<iframe loading="lazy"', $iframe);
}, $content);
return $content;
}
// Remove Query Strings
public function remove_query_strings($src) {
if (strpos($src, '?ver=') !== false) {
$src = remove_query_arg('ver', $src);
}
return $src;
}
// Disable Emojis
public function disable_emojis() {
remove_action('wp_head', 'print_emoji_detection_script', 7);
remove_action('admin_print_scripts', 'print_emoji_detection_script');
remove_action('wp_print_styles', 'print_emoji_styles');
remove_action('admin_print_styles', 'print_emoji_styles');
remove_filter('the_content_feed', 'wp_staticize_emoji');
remove_filter('comment_text_rss', 'wp_staticize_emoji');
remove_filter('wp_mail', 'wp_staticize_emoji_for_email');
add_filter('tiny_mce_plugins', function($plugins) {
return is_array($plugins) ? array_diff($plugins, ['wpemoji']) : [];
});
add_filter('wp_resource_hints', function($urls, $relation_type) {
if ('dns-prefetch' === $relation_type) {
$urls = array_filter($urls, function($url) {
return strpos($url, 'https://s.w.org/images/core/emoji/') === false;
});
}
return $urls;
}, 10, 2);
}
// Disable Embeds
public function disable_embeds() {
remove_action('rest_api_init', 'wp_oembed_register_route');
remove_filter('oembed_dataparse', 'wp_filter_oembed_result', 10);
remove_action('wp_head', 'wp_oembed_add_discovery_links');
remove_action('wp_head', 'wp_oembed_add_host_js');
add_filter('embed_oembed_discover', '__return_false');
add_filter('tiny_mce_plugins', function($plugins) {
return array_diff($plugins, ['wpembed']);
});
add_filter('rewrite_rules_array', function($rules) {
foreach ($rules as $rule => $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('/<img[^>]+>/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('<img', '<img fetchpriority="high"', $optimized_img);
}
// Change loading="lazy" to loading="eager" or add it
if (strpos($optimized_img, 'loading="lazy"') !== false) {
$optimized_img = str_replace('loading="lazy"', 'loading="eager"', $optimized_img);
} elseif (strpos($optimized_img, 'loading=') === false) {
$optimized_img = str_replace('<img', '<img loading="eager"', $optimized_img);
}
// Extract URL for preload
if (preg_match('/src=["\']([^"\']+)["\']/i', $optimized_img, $src_match)) {
$this->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 = '<link rel="preload" as="image" href="' . esc_url($this->lcp_image_url) . '"';
if ($this->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 '<link rel="preconnect" href="' . esc_url($domain) . '" crossorigin>' . "\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 '<link rel="preload" as="font" type="font/woff2" href="' . esc_url($font_url) . '" crossorigin>' . "\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('<script', '<script type="text/plain" data-jabali-delayed', $tag);
// Add the delay loader script if not already added
static $loader_added = false;
if (!$loader_added) {
add_action('wp_footer', [$this, 'output_delay_loader_script'], 999);
$loader_added = true;
}
return $tag;
}
/**
* Output script that loads delayed scripts on user interaction
*/
public function output_delay_loader_script() {
?>
<script>
(function() {
var loaded = false;
function loadDelayedScripts() {
if (loaded) return;
loaded = true;
document.querySelectorAll('script[data-jabali-delayed]').forEach(function(script) {
var newScript = document.createElement('script');
if (script.getAttribute('data-jabali-delay-src')) {
newScript.src = script.getAttribute('data-jabali-delay-src');
}
newScript.async = true;
Array.from(script.attributes).forEach(function(attr) {
if (attr.name !== 'type' && attr.name !== 'data-jabali-delayed' && attr.name !== 'data-jabali-delay-src') {
newScript.setAttribute(attr.name, attr.value);
}
});
if (script.innerHTML) {
newScript.innerHTML = script.innerHTML;
}
script.parentNode.replaceChild(newScript, script);
});
}
['scroll', 'mousemove', 'touchstart', 'keydown', 'click'].forEach(function(event) {
window.addEventListener(event, loadDelayedScripts, {once: true, passive: true});
});
setTimeout(loadDelayedScripts, 5000);
})();
</script>
<?php
}
/**
* Localize Google Fonts - intercept and serve locally
*/
public function localize_google_fonts_css($src, $handle) {
// Only process Google Fonts CSS URLs
if (strpos($src, 'fonts.googleapis.com/css') === false) {
return $src;
}
// Generate a cache key based on the URL
$cache_key = 'jabali_fonts_' . md5($src);
$cache_dir = WP_CONTENT_DIR . '/cache/jabali/fonts';
$cache_url = content_url('/cache/jabali/fonts');
// Check if we have a cached version
$cached_file = $cache_dir . '/' . $cache_key . '.css';
if (file_exists($cached_file) && (time() - filemtime($cached_file)) < WEEK_IN_SECONDS) {
return $cache_url . '/' . $cache_key . '.css';
}
// Create cache directory if it doesn't exist
if (!is_dir($cache_dir)) {
wp_mkdir_p($cache_dir);
}
// Fetch Google Fonts CSS with a modern user agent to get woff2
$response = wp_remote_get($src, [
'timeout' => 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;
}
?>
<style>
.jabali-header {
display: flex;
align-items: center;
gap: 16px;
margin: 20px 0;
padding-bottom: 20px;
border-bottom: 1px solid #c3c4c7;
}
.jabali-logo {
width: 48px;
height: 48px;
flex-shrink: 0;
}
.jabali-header-text h1 {
margin: 0 0 4px;
font-size: 23px;
font-weight: 400;
line-height: 1.3;
}
.jabali-header-text p {
margin: 0;
color: #646970;
}
.jabali-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-top: 20px;
}
@media (max-width: 782px) {
.jabali-grid {
grid-template-columns: 1fr;
}
}
.jabali-card {
background: #fff;
border: 1px solid #c3c4c7;
box-shadow: 0 1px 1px rgba(0,0,0,.04);
}
.jabali-card-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: #f6f7f7;
border-bottom: 1px solid #c3c4c7;
}
.jabali-card-header .dashicons {
color: #2271b1;
font-size: 20px;
width: 20px;
height: 20px;
}
.jabali-card-header h2 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.jabali-card-body {
padding: 16px;
}
.jabali-setting-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid #f0f0f1;
}
.jabali-setting-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.jabali-setting-row:first-child {
padding-top: 0;
}
.jabali-setting-info {
flex: 1;
padding-right: 16px;
}
.jabali-setting-label {
display: block;
font-weight: 600;
margin-bottom: 4px;
color: #1d2327;
}
.jabali-setting-desc {
font-size: 13px;
color: #646970;
line-height: 1.5;
}
.jabali-toggle {
position: relative;
width: 36px;
height: 18px;
flex-shrink: 0;
margin-top: 2px;
}
.jabali-toggle input {
opacity: 0;
width: 0;
height: 0;
}
.jabali-toggle-slider {
position: absolute;
cursor: pointer;
inset: 0;
background: #8c8f94;
border-radius: 18px;
transition: 0.2s;
}
.jabali-toggle-slider:before {
content: '';
position: absolute;
width: 14px;
height: 14px;
left: 2px;
bottom: 2px;
background: #fff;
border-radius: 50%;
transition: 0.2s;
}
.jabali-toggle input:checked + .jabali-toggle-slider {
background: #2271b1;
}
.jabali-toggle input:checked + .jabali-toggle-slider:before {
transform: translateX(18px);
}
.jabali-toggle input:focus + .jabali-toggle-slider {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #2271b1;
}
.jabali-stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.jabali-stat-box {
text-align: center;
padding: 12px 8px;
background: #f6f7f7;
border: 1px solid #c3c4c7;
}
.jabali-stat-value {
font-size: 20px;
font-weight: 600;
color: #1d2327;
}
.jabali-stat-value.success { color: #00a32a; }
.jabali-stat-value.warning { color: #dba617; }
.jabali-stat-value.error { color: #d63638; }
.jabali-stat-label {
font-size: 11px;
color: #646970;
text-transform: uppercase;
margin-top: 4px;
}
.jabali-actions {
display: flex;
gap: 8px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f1;
}
.jabali-info-notice {
background: #f0f6fc;
border-left: 4px solid #72aee6;
padding: 12px;
margin-top: 12px;
}
.jabali-info-notice p {
margin: 0;
font-size: 13px;
}
.jabali-warning-notice {
background: #fcf9e8;
border-left: 4px solid #dba617;
padding: 12px;
margin-bottom: 12px;
}
.jabali-warning-notice p {
margin: 0;
font-size: 13px;
}
.jabali-submit-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #c3c4c7;
}
.jabali-footer {
margin-top: 30px;
padding: 20px;
background: #fff;
border: 1px solid #c3c4c7;
}
.jabali-footer h3 {
margin: 0 0 8px;
font-size: 14px;
}
.jabali-footer p {
margin: 0 0 8px;
color: #646970;
}
.jabali-footer p:last-child {
margin-bottom: 0;
}
</style>
<?php
}
public function add_admin_bar_menu($wp_admin_bar) {
if (!current_user_can('manage_options')) {
return;
}
$connected = $this->is_redis_connected();
$wp_admin_bar->add_node([
'id' => 'jabali-cache',
'title' => sprintf(
'<span style="color: %s;">●</span> %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(
'<a href="%s">%s</a>',
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();
?>
<div class="wrap">
<?php if (isset($_GET['flushed'])): ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Object cache flushed successfully.', 'jabali-cache'); ?></p>
</div>
<?php endif; ?>
<?php if (isset($_GET['cleared'])): ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('All caches cleared successfully.', 'jabali-cache'); ?></p>
</div>
<?php endif; ?>
<?php if (isset($_GET['purged'])): ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Page cache purged successfully.', 'jabali-cache'); ?></p>
</div>
<?php endif; ?>
<?php if (isset($_GET['enabled'])): ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Object cache enabled successfully.', 'jabali-cache'); ?></p>
</div>
<?php endif; ?>
<?php if (isset($_GET['disabled'])): ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Object cache disabled successfully.', 'jabali-cache'); ?></p>
</div>
<?php endif; ?>
<?php if (isset($_GET['settings-updated'])): ?>
<div class="notice notice-success is-dismissible">
<p><?php _e('Settings saved successfully.', 'jabali-cache'); ?></p>
</div>
<?php endif; ?>
<!-- Header with Logo -->
<div class="jabali-header">
<svg class="jabali-logo" viewBox="0 0 147.26 96.573" xmlns="http://www.w3.org/2000/svg">
<g transform="translate(-34.391 -47.118)">
<g transform="translate(-3.541 -25.332)">
<path fill="#2271b1" d="m79.472 73.244c-4.0065 0.95163-8.277 4.0636-9.2604 8.2021h1.8521v0.52917c-5.293 0.24188-10.217 5.6941-15.081 7.7231-1.114 0.46467-4.676 1.2393-5.1043 2.4417-0.38218 1.073 1.0399 2.7743 1.5975 3.5935 2.4662 3.6227 5.8771 5.0144 10.121 3.9687-1.5506 5.3455-7.6205 6.9269-11.24 2.3802-3.2616-4.0969-3.312-10.11-3.312-15.08h0.26458v0.52917h0.26458l-1.0583-2.9104h-0.26458c-0.70254 1.8212-1.2726 3.6092-1.5118 5.5562-0.11276 0.91811 0.18619 2.2623-0.34767 3.0641-1.4776 2.2192-6.4692 0.93492-8.4593 0.37544 0.20517 2.3499 1.304 8.2501 3.0454 9.9049 1.8549 1.7628 6.4774 1.7367 8.8609 1.7367v0.26459l-3.175 0.52916v0.26459c7.2825 4.4949 15.112 11.721 18.785 19.579h0.26458c-0.01183-4.2856-2.0574-7.5834-4.2333-11.112 4.5342 3.0843 9.258 7.8497 14.552 9.525l-1.5875-5.2917h0.26458c0.84444 4.7343 6.8962 10.084 11.377 11.377-1.2956-5.6515-1.6033-9.8179 0.52917-15.346h0.26458c-2.5132 10.202 3.9699 16.869 9.2604 24.606l-14.023-5.5226-9.7896-7.1774c1.5844 2.7814 4.9469 6.0214 5.4293 9.2604 0.40039 2.6886-0.9934 6.31-2.0295 8.7312-2.8259 6.6039-7.6431 11.451-13.189 15.927-2.8919 2.334-6.6625 4.4647-8.9958 7.3563 4.0856 0 8.6528 1.3244 10.054-3.4396h0.26458v2.3812l6.6146-4.986 10.236-8.6639 1.5197-3.2003 5.9717-2.2681 4.4979-2.888 3.175-2.6291 5.5562 0.55836-3.9687-7.6729c3.7228 2.2978 7.9806 5.5165 10.332 9.2604 1.5432 2.4574 1.9452 5.5757 3.5984 7.9375 2.1589 3.0843 5.4308 5.7197 8.1059 8.3544 1.9816 1.9517 4.3458 5.5336 7.0692 6.4219 1.6243 0.52978 3.1232-1.294 2.7832-2.87-0.2334-1.0821-1.2485-1.8971-1.9905-2.6458l2.3812 0.52917c-0.0648-2.6792-1.8771-2.9045-3.7042-4.3366-2.004-1.5708-4.6598-4.1729-5.7406-6.5113-0.71228-1.5411-0.50959-3.8743-0.63288-5.5562-0.29051-3.9631-0.28284-8.2826-1.1249-12.171-0.40549-1.8724-2.2649-3.3993-3.2295-5.0271-1.422-2.3996-2.1467-5.1916-2.5013-7.9375h0.26458c0.93579 2.8711 2.3848 5.5274 4.2064 7.9375 1.0104 1.3367 2.6091 2.627 3.1628 4.2333 0.62267 1.8067 0.73913 4.1838 0.82253 6.0854 0.0342 0.77985-0.23556 2.0716 0.43464 2.6489 1.1663 1.0046 4.7066 0.78333 6.1904 0.79049 5.8026 0.028 12.063-0.25522 17.462-2.6456-0.77197-2.0367-2.2275-3.7778-3.0552-5.8208-1.5912-3.928-1.4637-7.2919-0.91359-11.377h0.26458c0 4.5242 1.0259 8.7475 3.2881 12.7 2.2975 4.0142 6.1699 8.2606 5.6616 13.229-0.88324 8.6337-11.013 11.804-12.918 19.844 3.1087 0 5.5125 0.6513 6.8792-2.6458h0.26458v1.8521c2.9712-1.5415 3.1185-4.5507 5.0836-6.8792 2.8528-3.3803 8.621-6.0668 10.516-10.054 0.93566-1.9692-1.193-4.2544-5e-3 -6.35 1.0544-1.8601 3.0177-3.2355 4.2421-5.0271 3.2956-4.822 5.503-10.318 6.0481-16.14 0.21876-2.3368-0.37598-5.0017-1.2819-7.1438-0.44877-1.0612-1.8038-2.5236-1.6796-3.7042 0.1146-1.0894 1.3436-2.0376 1.9205-2.9104 1.0226-1.5471 1.8056-3.4197 1.8682-5.2917 0.0962-2.8782-1.4065-5.0444-3.432-6.9241-0.85752-0.79578-2.9299-2.2711-2.5035-3.6541 0.34257-1.1112 1.5139-1.3081 2.5074-1.3268 1.6838-0.03161 3.4012 0.16952 5.0271-0.36094 2.3027-0.75128 3.8049-2.2333 6.35-2.2861-1.7653-2.0607-4.7845-2.9032-7.4083-2.9104l1.3229-1.0583v-0.26458c-1.7525 0-3.572-0.1527-5.2917 0.23498-1.0777 0.24295-2.2263 0.49727-3.175 1.0865-5.9935 3.7225-0.17836 9.6913 1.4638 14.024 1.0121 2.6706-0.24648 6.0015-3.0513 7.0055-1.3226 0.47346-2.5954-0.66607-3.7042-1.2419-2.3547-1.2228-4.8913-2.1202-7.4083-2.9406-8.2756-2.6974-17.5-4.146-26.194-4.146v-0.26458l4.4979-1.8521c-3.7368-2.2824-9.3016-1.8521-13.494-1.8521v-0.26458l7.4083-3.175v-0.26458l-14.552-0.79375v-0.26458l5.8208-2.3812v-0.26458c-4.5419-0.36146-9.4692-1.8779-14.023-0.79375l7.6729-4.4979c-3.1942-1.2051-9.5711-1.7051-12.666 0.08023-1.0502 0.60585-1.5876 1.9605-2.4392 2.8055-1.8354 1.8212-4.3154 2.6495-6.8553 2.6705v-0.26458c7.3819-1.0902 9.4631-9.4281 7.1438-15.61-3.6173 0.74663-7.4063 2.0646-10.315 4.4103-1.2534 1.0109-2.7138 4.3127-4.257 4.5994-1.0995 0.20428-1.4526-1.0575-1.6183-1.8662-0.45208-2.2056 0.32443-4.3161 1.109-6.3498m-11.112 15.875c-0.98324 2.0589-2.9464 2.3799-5.0271 2.3812 0.97375-2.0991 2.7849-3.4588 5.0271-2.3812m-6.8792 25.4-0.26458 0.26458 0.26458-0.26458m108.48 17.727c-1.2104 4.4255-4.1608 9.5105-7.9375 12.171v0.52916c3.1505 1.7947 7.6489 3.4777 10.315 5.8698 4.7761 4.2855 6.0439 12.285 5.5601 18.207 4.1709 0 7.4428-0.0408 5.8208-5.0271l1.3229 0.79375c0.87387-3.6809-2.2376-6.5639-3.5443-9.7896-1.5868-3.9171-1.5007-8.3848-2.9653-12.16-0.67252-1.7337-3.2676-2.9299-4.5271-4.2674-1.79-1.9009-2.4102-4.4741-4.0445-6.3263m13.758 31.485-0.26459 0.26459 0.26459-0.26459m-38.629 1.8521-0.26458 0.26458z"/>
</g>
</g>
</svg>
<div class="jabali-header-text">
<h1><?php _e('Jabali Cache', 'jabali-cache'); ?></h1>
<p><?php _e('High-performance caching and optimization for WordPress', 'jabali-cache'); ?></p>
</div>
</div>
<form method="post" action="options.php">
<?php settings_fields(self::OPTION_KEY); ?>
<div class="jabali-grid">
<!-- Page Cache Card -->
<div class="jabali-card">
<div class="jabali-card-header">
<span class="dashicons dashicons-database"></span>
<h2><?php _e('Page Cache', 'jabali-cache'); ?></h2>
</div>
<div class="jabali-card-body">
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Enable Page Cache', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Full page caching via nginx. Dramatically speeds up page load times by serving cached HTML directly from the server.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[page_cache]" value="1" <?php checked($this->settings['page_cache']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-info-notice">
<p><strong><?php _e('Note:', 'jabali-cache'); ?></strong> <?php _e('Cache is automatically bypassed for logged-in users, POST requests, and WooCommerce cart/checkout pages. Content changes trigger automatic cache purging.', 'jabali-cache'); ?></p>
</div>
</div>
</div>
<!-- Object Cache Card -->
<div class="jabali-card">
<div class="jabali-card-header">
<span class="dashicons dashicons-performance"></span>
<h2><?php _e('Object Cache (Redis)', 'jabali-cache'); ?></h2>
</div>
<div class="jabali-card-body">
<div class="jabali-stats-grid">
<div class="jabali-stat-box">
<div class="jabali-stat-value <?php echo $redis_connected ? 'success' : 'error'; ?>">
<?php echo $redis_connected ? __('Connected', 'jabali-cache') : __('Offline', 'jabali-cache'); ?>
</div>
<div class="jabali-stat-label"><?php _e('Status', 'jabali-cache'); ?></div>
</div>
<div class="jabali-stat-box">
<div class="jabali-stat-value <?php echo $stats['ratio'] >= 80 ? 'success' : ($stats['ratio'] >= 50 ? 'warning' : 'error'); ?>">
<?php echo $stats['ratio']; ?>%
</div>
<div class="jabali-stat-label"><?php _e('Hit Ratio', 'jabali-cache'); ?></div>
</div>
<div class="jabali-stat-box">
<div class="jabali-stat-value">
<?php echo number_format($stats['hits']); ?>
</div>
<div class="jabali-stat-label"><?php _e('Hits', 'jabali-cache'); ?></div>
</div>
<div class="jabali-stat-box">
<div class="jabali-stat-value">
<?php echo number_format($stats['misses']); ?>
</div>
<div class="jabali-stat-label"><?php _e('Misses', 'jabali-cache'); ?></div>
</div>
</div>
<?php if ($stats['ratio'] < 50 && ($stats['hits'] + $stats['misses']) > 100): ?>
<div class="jabali-warning-notice">
<p><strong><?php _e('Low Hit Ratio:', 'jabali-cache'); ?></strong> <?php _e('Consider reviewing your caching strategy. High misses may indicate frequent cache flushes or too many unique cache keys.', 'jabali-cache'); ?></p>
</div>
<?php endif; ?>
<div class="jabali-actions">
<?php if ($drop_in_installed): ?>
<a href="<?php echo wp_nonce_url(admin_url('options-general.php?page=jabali-cache&action=flush'), 'jabali_cache_flush'); ?>" class="button button-primary">
<?php _e('Flush Cache', 'jabali-cache'); ?>
</a>
<a href="<?php echo wp_nonce_url(admin_url('options-general.php?page=jabali-cache&action=disable'), 'jabali_cache_disable'); ?>" class="button">
<?php _e('Disable', 'jabali-cache'); ?>
</a>
<?php elseif ($redis_available): ?>
<a href="<?php echo wp_nonce_url(admin_url('options-general.php?page=jabali-cache&action=enable'), 'jabali_cache_enable'); ?>" class="button button-primary">
<?php _e('Enable Object Cache', 'jabali-cache'); ?>
</a>
<?php else: ?>
<p class="description"><?php _e('Redis PHP extension is not installed.', 'jabali-cache'); ?></p>
<?php endif; ?>
</div>
</div>
</div>
<!-- Minification Card -->
<div class="jabali-card">
<div class="jabali-card-header">
<span class="dashicons dashicons-editor-code"></span>
<h2><?php _e('Minification', 'jabali-cache'); ?></h2>
</div>
<div class="jabali-card-body">
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('HTML Minification', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Remove whitespace and comments from HTML output.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[html_minify]" value="1" <?php checked($this->settings['html_minify']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('CSS Minification', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Minify inline CSS styles.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[minify_css]" value="1" <?php checked($this->settings['minify_css']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('JS Minification', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Minify inline JavaScript (conservative).', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[minify_js]" value="1" <?php checked($this->settings['minify_js']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Media Card -->
<div class="jabali-card">
<div class="jabali-card-header">
<span class="dashicons dashicons-format-image"></span>
<h2><?php _e('Media', 'jabali-cache'); ?></h2>
</div>
<div class="jabali-card-body">
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Lazy Load Images', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Defer loading of images until they enter the viewport.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[lazy_load]" value="1" <?php checked($this->settings['lazy_load']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Lazy Load iframes', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Defer loading of embedded videos and iframes.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[lazy_load_iframes]" value="1" <?php checked($this->settings['lazy_load_iframes']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Local Google Fonts', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Download and serve Google Fonts locally for faster loading and GDPR compliance.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[local_google_fonts]" value="1" <?php checked($this->settings['local_google_fonts']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- JavaScript Card -->
<div class="jabali-card">
<div class="jabali-card-header">
<span class="dashicons dashicons-media-code"></span>
<h2><?php _e('JavaScript', 'jabali-cache'); ?></h2>
</div>
<div class="jabali-card-body">
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Defer JavaScript', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Add defer attribute to non-critical scripts for faster page rendering.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[defer_js]" value="1" <?php checked($this->settings['defer_js']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Remove Query Strings', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Remove version query strings from static resources.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[remove_query_strings]" value="1" <?php checked($this->settings['remove_query_strings']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- Cleanup Card -->
<div class="jabali-card">
<div class="jabali-card-header">
<span class="dashicons dashicons-trash"></span>
<h2><?php _e('Cleanup', 'jabali-cache'); ?></h2>
</div>
<div class="jabali-card-body">
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Disable Emojis', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Remove WordPress emoji scripts and styles.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[disable_emojis]" value="1" <?php checked($this->settings['disable_emojis']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Disable oEmbed', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Disable WordPress embed discovery and related scripts.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[disable_embeds]" value="1" <?php checked($this->settings['disable_embeds']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
</div>
</div>
<!-- LCP Optimization Card -->
<div class="jabali-card">
<div class="jabali-card-header">
<span class="dashicons dashicons-performance"></span>
<h2><?php _e('LCP Optimization', 'jabali-cache'); ?></h2>
</div>
<div class="jabali-card-body">
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Preload LCP Image', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Detect and preload the largest image above the fold. Adds fetchpriority="high" and loading="eager" to speed up LCP.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[lcp_preload]" value="1" <?php checked($this->settings['lcp_preload']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Preconnect Hints', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Add preconnect hints for external domains (fonts, CDN) to reduce connection time.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[preconnect_hints]" value="1" <?php checked($this->settings['preconnect_hints']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Preload Fonts', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Preload critical web fonts to prevent late text rendering. Works best with Local Google Fonts enabled.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[preload_fonts]" value="1" <?php checked($this->settings['preload_fonts']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Delay Third-Party Scripts', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Delay analytics, chat widgets, and tracking scripts until user interaction. Improves initial page load.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[delay_third_party]" value="1" <?php checked($this->settings['delay_third_party']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-info-notice">
<p><strong><?php _e('LCP Tips:', 'jabali-cache'); ?></strong> <?php _e('LCP (Largest Contentful Paint) measures when the main content is visible. Enable Page Cache + LCP Preload for best results. Test with GTmetrix or PageSpeed Insights.', 'jabali-cache'); ?></p>
</div>
</div>
</div>
<!-- Developer Card -->
<div class="jabali-card">
<div class="jabali-card-header">
<span class="dashicons dashicons-admin-tools"></span>
<h2><?php _e('Developer', 'jabali-cache'); ?></h2>
</div>
<div class="jabali-card-body">
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Development Mode', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Disable all caching. Use during development to always see changes immediately.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[expired_cache]" value="1" <?php checked($this->settings['expired_cache']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-setting-row">
<div class="jabali-setting-info">
<span class="jabali-setting-label"><?php _e('Debug Mode', 'jabali-cache'); ?></span>
<span class="jabali-setting-desc"><?php _e('Show cache debug info in HTML comments when ?cache_debug=1 is added to URL.', 'jabali-cache'); ?></span>
</div>
<label class="jabali-toggle">
<input type="checkbox" name="<?php echo self::OPTION_KEY; ?>[cache_debug]" value="1" <?php checked($this->settings['cache_debug']); ?>>
<span class="jabali-toggle-slider"></span>
</label>
</div>
<div class="jabali-info-notice">
<p><strong><?php _e('Tip:', 'jabali-cache'); ?></strong> <?php _e('Check response headers for X-Cache (HIT/MISS) and X-Cache-Reason to diagnose caching behavior.', 'jabali-cache'); ?></p>
</div>
</div>
</div>
</div>
<div class="jabali-submit-row">
<?php submit_button(__('Save Settings', 'jabali-cache'), 'primary', 'submit', false); ?>
<a href="<?php echo wp_nonce_url(admin_url('options-general.php?page=jabali-cache&action=clear_all'), 'jabali_cache_clear_all'); ?>" class="button">
<?php _e('Clear All Caches', 'jabali-cache'); ?>
</a>
<a href="<?php echo wp_nonce_url(admin_url('options-general.php?page=jabali-cache&action=purge_page'), 'jabali_cache_purge_page'); ?>" class="button">
<?php _e('Purge Page Cache', 'jabali-cache'); ?>
</a>
</div>
</form>
<div class="jabali-footer">
<h3><?php _e('About Jabali Cache', 'jabali-cache'); ?></h3>
<p><?php _e('Jabali Cache provides comprehensive caching and optimization for WordPress sites hosted on Jabali Panel. It includes Redis object caching, nginx page caching with smart auto-purging, HTML/CSS/JS minification, lazy loading, local Google Fonts, and various performance optimizations.', 'jabali-cache'); ?></p>
<p>
<strong><?php _e('Version:', 'jabali-cache'); ?></strong> <?php echo self::VERSION; ?> &nbsp;|&nbsp;
<strong><?php _e('Smart Cache Purge:', 'jabali-cache'); ?></strong> <?php echo $this->settings['page_cache'] ? '<span style="color:#00a32a;">' . __('Active', 'jabali-cache') . '</span>' : '<span style="color:#646970;">' . __('Inactive', 'jabali-cache') . '</span>'; ?>
</p>
</div>
</div>
<?php
}
}
// Initialize
Jabali_Cache_Plugin::get_instance();
// Activation - enable object cache and page cache
register_activation_hook(__FILE__, function() {
$plugin = Jabali_Cache_Plugin::get_instance();
// Enable Redis object cache if available
if (class_exists('Redis')) {
$plugin->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();
});