PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.1.2
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.1.2
3.2.2 3.2.3 3.2.1 3.2.0 3.1.9 3.1.8 3.1.7 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.9 trunk 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.3 1.1.4 1.1.5 All 174 releases
master-addons / inc / classes / cache-manager.php

cache-manager.php in Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits 3.1.2, at inc/classes/cache-manager.php

1,041 lines 32.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Master Addons - Cache Manager
5 *
6 * Bundles per-page assets into single optimized CSS/JS files.
7 * Stores cached files in /wp-content/uploads/master_addons/assets_cache/
8 * with database fallback for metadata tracking.
9 *
10 * @package MasterAddons\Inc\Classes
11 * @since 2.0.0
12 * @see docs/plans/2026-01-12-vite-migration-asset-management-design.md
13 */
14
15 namespace MasterAddons\Inc\Classes;
16
17 if (!defined('ABSPATH')) {
18 exit;
19 }
20
21 class Cache_Manager
22 {
23 /**
24 * Singleton instance
25 */
26 private static $instance = null;
27
28 /**
29 * Cache directory name (inside uploads)
30 */
31 const CACHE_DIR = 'master_addons/assets_cache';
32
33 /**
34 * Database option for global cache metadata
35 */
36 const CACHE_META_OPTION = 'jltma_cache_meta';
37
38 /**
39 * Transient prefix for per-post cache info
40 */
41 const POST_CACHE_PREFIX = 'jltma_post_cache_';
42
43 /**
44 * Option name for cache enabled setting
45 */
46 const OPTION_KEY = 'jltma_cache_enabled';
47
48 /**
49 * Cache path (filesystem)
50 */
51 private $cache_path;
52
53 /**
54 * Cache URL
55 */
56 private $cache_url;
57
58 /**
59 * Get singleton instance
60 */
61 public static function get_instance()
62 {
63 if (null === self::$instance) {
64 self::$instance = new self();
65 }
66 return self::$instance;
67 }
68
69 /**
70 * Constructor
71 */
72 public function __construct()
73 {
74 $this->setup_cache_directory();
75
76 // Listen for cache invalidation
77 add_action('jltma/cache/invalidate_post', [$this, 'invalidate_post_cache']);
78 add_action('jltma/cache/clear_all', [$this, 'clear_all_cache']);
79
80 // Hook into asset loading when caching enabled
81 if ($this->is_enabled()) {
82 add_action('wp_enqueue_scripts', [$this, 'maybe_serve_cached_bundle'], 99);
83 }
84
85 // Clear cache on theme/plugin updates
86 add_action('switch_theme', [$this, 'clear_all_cache']);
87 add_action('upgrader_process_complete', [$this, 'on_upgrade_complete'], 10, 2);
88
89 // AJAX handlers for admin
90 add_action('wp_ajax_jltma_clear_cache', [$this, 'ajax_clear_cache']);
91 add_action('wp_ajax_jltma_regenerate_cache', [$this, 'ajax_regenerate_cache']);
92 add_action('wp_ajax_jltma_get_cache_stats', [$this, 'ajax_get_cache_stats']);
93 add_action('wp_ajax_jltma_clear_single_cache', [$this, 'ajax_clear_single_cache']);
94 add_action('wp_ajax_jltma_regenerate_single_cache', [$this, 'ajax_regenerate_single_cache']);
95 add_action('wp_ajax_jltma_save_performance_settings', [$this, 'ajax_save_performance_settings']);
96 }
97
98 /**
99 * Setup cache directory in uploads
100 */
101 private function setup_cache_directory()
102 {
103 $upload_dir = wp_upload_dir();
104
105 if (!empty($upload_dir['error']) || empty($upload_dir['basedir'])) {
106 return;
107 }
108
109 $this->cache_path = $upload_dir['basedir'] . '/' . self::CACHE_DIR;
110 $this->cache_url = $upload_dir['baseurl'] . '/' . self::CACHE_DIR;
111
112 // Create directory if it doesn't exist
113 if (!file_exists($this->cache_path)) {
114 wp_mkdir_p($this->cache_path);
115
116 // Add index.php for security
117 file_put_contents(
118 $this->cache_path . '/index.php',
119 '<?php // Silence is golden'
120 );
121
122 // Add .htaccess for gzip and caching
123 $htaccess = <<<'HTACCESS'
124 # Master Addons Cache with Gzip Support
125
126 # Enable gzip compression for CSS and JS
127 <IfModule mod_deflate.c>
128 AddOutputFilterByType DEFLATE text/css
129 AddOutputFilterByType DEFLATE application/javascript
130 AddOutputFilterByType DEFLATE text/javascript
131 </IfModule>
132
133 # Serve pre-compressed .gz files if they exist
134 <IfModule mod_rewrite.c>
135 RewriteEngine On
136
137 # Check if browser accepts gzip
138 RewriteCond %{HTTP:Accept-Encoding} gzip
139
140 # Serve .css.gz for .css requests
141 RewriteCond %{REQUEST_FILENAME}.gz -f
142 RewriteRule ^(.+)\.(css|js)$ $1.$2.gz [L]
143 </IfModule>
144
145 # Set correct content types for .gz files
146 <IfModule mod_mime.c>
147 AddType text/css .css.gz
148 AddType application/javascript .js.gz
149 AddEncoding gzip .gz
150 </IfModule>
151
152 # Cache control headers
153 <IfModule mod_headers.c>
154 Header set Cache-Control "max-age=31536000, public"
155
156 # Vary header for gzip
157 <FilesMatch "\.(css|js)(\.gz)?$">
158 Header append Vary Accept-Encoding
159 </FilesMatch>
160
161 # Content-Encoding for .gz files
162 <FilesMatch "\.gz$">
163 Header set Content-Encoding gzip
164 </FilesMatch>
165 </IfModule>
166 HTACCESS;
167 file_put_contents($this->cache_path . '/.htaccess', $htaccess);
168 }
169 }
170
171 /**
172 * Check if caching is enabled
173 */
174 public function is_enabled()
175 {
176 return (bool) get_option(self::OPTION_KEY, false);
177 }
178
179 /**
180 * Enable caching
181 */
182 public static function enable()
183 {
184 update_option(self::OPTION_KEY, true);
185 }
186
187 /**
188 * Disable caching
189 */
190 public static function disable()
191 {
192 update_option(self::OPTION_KEY, false);
193 }
194
195 /**
196 * Maybe serve cached bundle for current post
197 */
198 public function maybe_serve_cached_bundle()
199 {
200 // Skip in admin or editor
201 if (is_admin() || $this->is_elementor_editor()) {
202 return;
203 }
204
205 $post_id = get_the_ID();
206 if (!$post_id) {
207 return;
208 }
209
210 // Check for existing valid cache
211 $cache_info = $this->get_post_cache_info($post_id);
212
213 if ($cache_info && $this->is_cache_valid($cache_info)) {
214 $this->serve_cached_bundle($cache_info);
215 return;
216 }
217
218 // Generate new cache on shutdown (non-blocking)
219 add_action('shutdown', function () use ($post_id) {
220 $this->generate_post_cache($post_id);
221 });
222 }
223
224 /**
225 * Generate bundled CSS/JS for a post
226 */
227 public function generate_post_cache($post_id)
228 {
229 $assets_loader = Assets_Loader::get_instance();
230 $widgets = $assets_loader->detect_page_widgets($post_id);
231
232 if (empty($widgets)) {
233 return false;
234 }
235
236 // Generate unique hash based on widgets used
237 $hash = $this->generate_cache_hash($widgets);
238
239 // Bundle CSS
240 $css_content = $this->bundle_css_files($widgets);
241 $css_filename = "post-{$post_id}-{$hash}.css";
242
243 // Bundle JS
244 $js_content = $this->bundle_js_files($widgets);
245 $js_filename = "post-{$post_id}-{$hash}.js";
246
247 // Write files with gzip compression
248 $css_result = ['written' => false, 'gzip_written' => false, 'gzip_size' => 0];
249 $js_result = ['written' => false, 'gzip_written' => false, 'gzip_size' => 0];
250
251 if (!empty($css_content)) {
252 $css_result = $this->write_with_gzip(
253 $this->cache_path . '/' . $css_filename,
254 $css_content
255 );
256 }
257
258 if (!empty($js_content)) {
259 $js_result = $this->write_with_gzip(
260 $this->cache_path . '/' . $js_filename,
261 $js_content
262 );
263 }
264
265 if (!$css_result['written'] && !$js_result['written']) {
266 // File write failed - store in database fallback
267 $this->store_in_database($post_id, $css_content, $js_content);
268 return false;
269 }
270
271 // Store cache metadata
272 $cache_info = [
273 'hash' => $hash,
274 'css_file' => $css_result['written'] ? $css_filename : null,
275 'js_file' => $js_result['written'] ? $js_filename : null,
276 'widgets' => $widgets,
277 'created' => time(),
278 'size_css' => strlen($css_content),
279 'size_js' => strlen($js_content),
280 'gzip_size_css' => $css_result['gzip_size'],
281 'gzip_size_js' => $js_result['gzip_size'],
282 'gzip_enabled' => $css_result['gzip_written'] || $js_result['gzip_written'],
283 'is_rtl' => is_rtl(),
284 ];
285
286 set_transient(
287 self::POST_CACHE_PREFIX . $post_id,
288 $cache_info,
289 WEEK_IN_SECONDS
290 );
291
292 $this->update_global_cache_meta($post_id, $cache_info);
293
294 return true;
295 }
296
297 /**
298 * Bundle multiple CSS files into one
299 * Handles array format from JLTMA_Config
300 */
301 private function bundle_css_files($widgets)
302 {
303 $assets_loader = Assets_Loader::get_instance();
304 $widget_assets = $assets_loader->get_widget_assets();
305 $is_rtl = is_rtl();
306
307 $bundled_css = "/* Master Addons Bundled CSS - " . gmdate('Y-m-d H:i:s') . " */\n";
308 $processed = [];
309
310 foreach ($widgets as $widget_name) {
311 if (!isset($widget_assets[$widget_name])) {
312 continue;
313 }
314
315 // CSS is now an array
316 $css_files = $widget_assets[$widget_name]['css'] ?? [];
317
318 if (empty($css_files)) {
319 continue;
320 }
321
322 foreach ((array) $css_files as $css_slug) {
323 if (isset($processed[$css_slug])) {
324 continue;
325 }
326
327 $css_file = JLTMA_PATH . "assets/css/addons/{$css_slug}.css";
328
329 // Use RTL file if site is RTL
330 if ($is_rtl) {
331 $rtl_file = JLTMA_PATH . "assets/css/addons/{$css_slug}.rtl.css";
332 if (file_exists($rtl_file)) {
333 $css_file = $rtl_file;
334 }
335 }
336
337 if (file_exists($css_file)) {
338 $bundled_css .= "/* Widget: {$widget_name} ({$css_slug}) */\n";
339 $bundled_css .= file_get_contents($css_file) . "\n";
340 $processed[$css_slug] = true;
341 }
342 }
343
344 // Also bundle vendor CSS
345 $vendor_css = $widget_assets[$widget_name]['vendor']['css'] ?? [];
346 foreach ((array) $vendor_css as $vendor_slug) {
347 if (isset($processed['vendor-' . $vendor_slug])) {
348 continue;
349 }
350
351 $vendor_file = JLTMA_PATH . "assets/vendor/{$vendor_slug}/{$vendor_slug}.css";
352 if (file_exists($vendor_file)) {
353 $bundled_css .= "/* Vendor: {$vendor_slug} */\n";
354 $bundled_css .= file_get_contents($vendor_file) . "\n";
355 $processed['vendor-' . $vendor_slug] = true;
356 }
357 }
358 }
359
360 // Add common swiper styles if needed
361 $swiper_widgets = ['ma-logo-slider', 'ma-team-members-slider', 'ma-image-carousel', 'ma-twitter-slider', 'ma-blog', 'ma-timeline'];
362 if (array_intersect($widgets, $swiper_widgets)) {
363 $swiper_file = JLTMA_PATH . 'assets/css/common/swiper-carousel.css';
364 if (file_exists($swiper_file) && !isset($processed['common-swiper-carousel'])) {
365 $bundled_css .= "/* Common: Swiper */\n";
366 $bundled_css .= file_get_contents($swiper_file) . "\n";
367 }
368 }
369
370 return $this->minify_css($bundled_css);
371 }
372
373 /**
374 * Bundle multiple JS files into one
375 * Handles array format from JLTMA_Config
376 */
377 private function bundle_js_files($widgets)
378 {
379 $assets_loader = Assets_Loader::get_instance();
380 $widget_assets = $assets_loader->get_widget_assets();
381
382 $bundled_js = "/* Master Addons Bundled JS - " . gmdate('Y-m-d H:i:s') . " */\n";
383 $bundled_js .= "(function($){\n'use strict';\n";
384
385 $has_content = false;
386 $processed = [];
387
388 foreach ($widgets as $widget_name) {
389 if (!isset($widget_assets[$widget_name])) {
390 continue;
391 }
392
393 // JS is now an array
394 $js_files = $widget_assets[$widget_name]['js'] ?? [];
395
396 foreach ((array) $js_files as $js_slug) {
397 if (empty($js_slug) || isset($processed[$js_slug])) {
398 continue;
399 }
400
401 $js_file = JLTMA_PATH . "assets/js/addons/{$js_slug}.js";
402
403 if (file_exists($js_file)) {
404 $bundled_js .= "/* Widget: {$widget_name} ({$js_slug}) */\n";
405 $bundled_js .= file_get_contents($js_file) . "\n";
406 $processed[$js_slug] = true;
407 $has_content = true;
408 }
409 }
410
411 // Also bundle vendor JS
412 $vendor_js = $widget_assets[$widget_name]['vendor']['js'] ?? [];
413 foreach ((array) $vendor_js as $vendor_slug) {
414 if (isset($processed['vendor-' . $vendor_slug])) {
415 continue;
416 }
417
418 $vendor_file = JLTMA_PATH . "assets/vendor/{$vendor_slug}/{$vendor_slug}.js";
419 if (file_exists($vendor_file)) {
420 $bundled_js .= "/* Vendor: {$vendor_slug} */\n";
421 $bundled_js .= file_get_contents($vendor_file) . "\n";
422 $processed['vendor-' . $vendor_slug] = true;
423 $has_content = true;
424 }
425 }
426 }
427
428 $bundled_js .= "})(jQuery);";
429
430 return $has_content ? $bundled_js : '';
431 }
432
433 /**
434 * Serve cached bundle instead of individual files
435 * Handles array format from JLTMA_Config
436 */
437 private function serve_cached_bundle($cache_info)
438 {
439 $assets_loader = Assets_Loader::get_instance();
440 $widget_assets = $assets_loader->get_widget_assets();
441
442 // Dequeue individual addon assets
443 foreach ($cache_info['widgets'] as $widget_name) {
444 if (!isset($widget_assets[$widget_name])) {
445 continue;
446 }
447
448 // CSS is now an array
449 $css_files = $widget_assets[$widget_name]['css'] ?? [];
450 foreach ((array) $css_files as $css_slug) {
451 wp_dequeue_style('jltma-' . $css_slug);
452 wp_dequeue_style('jltma-' . $css_slug . '-rtl');
453 }
454
455 // JS is now an array
456 $js_files = $widget_assets[$widget_name]['js'] ?? [];
457 foreach ((array) $js_files as $js_slug) {
458 wp_dequeue_script('jltma-' . $js_slug);
459 }
460
461 // Also dequeue vendor assets
462 $vendor_css = $widget_assets[$widget_name]['vendor']['css'] ?? [];
463 foreach ((array) $vendor_css as $vendor_slug) {
464 wp_dequeue_style('jltma-vendor-' . $vendor_slug);
465 }
466
467 $vendor_js = $widget_assets[$widget_name]['vendor']['js'] ?? [];
468 foreach ((array) $vendor_js as $vendor_slug) {
469 wp_dequeue_script('jltma-vendor-' . $vendor_slug);
470 }
471 }
472
473 // Also dequeue common swiper if cached
474 wp_dequeue_style('jltma-swiper-carousel');
475
476 // Enqueue bundled CSS
477 if (!empty($cache_info['css_file'])) {
478 wp_enqueue_style(
479 'jltma-bundled-' . $cache_info['hash'],
480 $this->cache_url . '/' . $cache_info['css_file'],
481 [],
482 JLTMA_VER
483 );
484 }
485
486 // Enqueue bundled JS
487 if (!empty($cache_info['js_file'])) {
488 wp_enqueue_script(
489 'jltma-bundled-' . $cache_info['hash'],
490 $this->cache_url . '/' . $cache_info['js_file'],
491 ['jquery'],
492 JLTMA_VER,
493 true
494 );
495 }
496 }
497
498 /**
499 * Generate hash from widget list
500 */
501 private function generate_cache_hash($widgets)
502 {
503 sort($widgets); // Consistent ordering
504 $rtl_suffix = is_rtl() ? '-rtl' : '';
505 return substr(md5(implode('|', $widgets) . JLTMA_VER . $rtl_suffix), 0, 8);
506 }
507
508 /**
509 * Check if cache is still valid
510 */
511 private function is_cache_valid($cache_info)
512 {
513 // Check if RTL setting changed
514 if (isset($cache_info['is_rtl']) && $cache_info['is_rtl'] !== is_rtl()) {
515 return false;
516 }
517
518 // Check if files exist
519 if (!empty($cache_info['css_file'])) {
520 if (!file_exists($this->cache_path . '/' . $cache_info['css_file'])) {
521 return false;
522 }
523 }
524
525 if (!empty($cache_info['js_file'])) {
526 if (!file_exists($this->cache_path . '/' . $cache_info['js_file'])) {
527 return false;
528 }
529 }
530
531 // Check if plugin version changed (hash includes version)
532 $current_hash = $this->generate_cache_hash($cache_info['widgets']);
533 if ($current_hash !== $cache_info['hash']) {
534 return false;
535 }
536
537 return true;
538 }
539
540 /**
541 * Invalidate cache for a specific post
542 */
543 public function invalidate_post_cache($post_id)
544 {
545 $cache_info = $this->get_post_cache_info($post_id);
546
547 if ($cache_info) {
548 // Delete cached files (including gzipped versions)
549 if (!empty($cache_info['css_file'])) {
550 wp_delete_file($this->cache_path . '/' . $cache_info['css_file']);
551 wp_delete_file($this->cache_path . '/' . $cache_info['css_file'] . '.gz');
552 }
553 if (!empty($cache_info['js_file'])) {
554 wp_delete_file($this->cache_path . '/' . $cache_info['js_file']);
555 wp_delete_file($this->cache_path . '/' . $cache_info['js_file'] . '.gz');
556 }
557
558 // Clear transient
559 delete_transient(self::POST_CACHE_PREFIX . $post_id);
560
561 // Clear database fallback
562 delete_post_meta($post_id, '_jltma_cached_css');
563 delete_post_meta($post_id, '_jltma_cached_js');
564 delete_post_meta($post_id, '_jltma_cache_in_db');
565
566 // Update global meta
567 $this->remove_from_global_cache_meta($post_id);
568 }
569 }
570
571 /**
572 * Clear all cache files and metadata
573 */
574 public function clear_all_cache()
575 {
576 // Delete all cache files (including gzipped versions)
577 $files = glob($this->cache_path . '/*.{css,js,css.gz,js.gz}', GLOB_BRACE);
578
579 if ($files) {
580 foreach ($files as $file) {
581 wp_delete_file($file);
582 }
583 }
584
585 // Clear all transients (using global meta to find them)
586 $global_meta = get_option(self::CACHE_META_OPTION, []);
587
588 if (!empty($global_meta['posts'])) {
589 foreach (array_keys($global_meta['posts']) as $post_id) {
590 delete_transient(self::POST_CACHE_PREFIX . $post_id);
591 delete_post_meta($post_id, '_jltma_cached_css');
592 delete_post_meta($post_id, '_jltma_cached_js');
593 delete_post_meta($post_id, '_jltma_cache_in_db');
594 }
595 }
596
597 // Reset global meta
598 update_option(self::CACHE_META_OPTION, [
599 'posts' => [],
600 'total_size' => 0,
601 'file_count' => 0,
602 'last_cleared' => time(),
603 ]);
604
605 return true;
606 }
607
608 /**
609 * Update global cache metadata for dashboard
610 */
611 private function update_global_cache_meta($post_id, $cache_info)
612 {
613 $global_meta = get_option(self::CACHE_META_OPTION, [
614 'posts' => [],
615 'total_size' => 0,
616 'file_count' => 0,
617 'last_cleared' => null,
618 ]);
619
620 // Remove old entry size if updating
621 if (isset($global_meta['posts'][$post_id])) {
622 $old = $global_meta['posts'][$post_id];
623 $global_meta['total_size'] -= ($old['size_css'] ?? 0) + ($old['size_js'] ?? 0);
624 $global_meta['file_count'] -= 2;
625 }
626
627 // Add new entry
628 $global_meta['posts'][$post_id] = [
629 'hash' => $cache_info['hash'],
630 'size_css' => $cache_info['size_css'],
631 'size_js' => $cache_info['size_js'],
632 'widgets' => $cache_info['widgets'],
633 'created' => $cache_info['created'],
634 'title' => get_the_title($post_id),
635 ];
636
637 $global_meta['total_size'] += $cache_info['size_css'] + $cache_info['size_js'];
638 $global_meta['file_count'] += 2;
639
640 update_option(self::CACHE_META_OPTION, $global_meta);
641 }
642
643 /**
644 * Remove post from global cache meta
645 */
646 private function remove_from_global_cache_meta($post_id)
647 {
648 $global_meta = get_option(self::CACHE_META_OPTION, []);
649
650 if (isset($global_meta['posts'][$post_id])) {
651 $entry = $global_meta['posts'][$post_id];
652 $global_meta['total_size'] -= ($entry['size_css'] ?? 0) + ($entry['size_js'] ?? 0);
653 $global_meta['file_count'] -= 2;
654 unset($global_meta['posts'][$post_id]);
655
656 update_option(self::CACHE_META_OPTION, $global_meta);
657 }
658 }
659
660 /**
661 * Get post cache info from transient
662 */
663 public function get_post_cache_info($post_id)
664 {
665 return get_transient(self::POST_CACHE_PREFIX . $post_id);
666 }
667
668 /**
669 * Get cache statistics for dashboard
670 */
671 public function get_cache_stats()
672 {
673 $global_meta = get_option(self::CACHE_META_OPTION, []);
674
675 // Verify actual files match metadata
676 $actual_files = glob($this->cache_path . '/*.{css,js}', GLOB_BRACE);
677 $actual_count = $actual_files ? count($actual_files) : 0;
678
679 // Calculate actual size
680 $actual_size = 0;
681 if ($actual_files) {
682 foreach ($actual_files as $file) {
683 $actual_size += filesize($file);
684 }
685 }
686
687 // Get cached files list with details
688 $cached_files = [];
689 if (!empty($global_meta['posts'])) {
690 foreach ($global_meta['posts'] as $post_id => $info) {
691 $css_file = $this->cache_path . "/post-{$post_id}-{$info['hash']}.css";
692 $file_size = 0;
693 $file_modified = 0;
694
695 if (file_exists($css_file)) {
696 $file_size = filesize($css_file);
697 $file_modified = filemtime($css_file);
698 }
699
700 $cached_files[] = [
701 'post_id' => $post_id,
702 'post_title' => $info['title'] ?? get_the_title($post_id),
703 'filename' => "post-{$post_id}-{$info['hash']}.css",
704 'size' => $file_size,
705 'size_formatted' => size_format($file_size),
706 'modified' => $file_modified,
707 'modified_formatted' => $file_modified ? human_time_diff($file_modified) . ' ' . __('ago', 'master-addons') : __('N/A', 'master-addons'),
708 'widgets' => $info['widgets'] ?? [],
709 ];
710 }
711 }
712
713 // Format last cleared time
714 $last_cleared = $global_meta['last_cleared'] ?? null;
715 $last_cleared_formatted = $last_cleared
716 ? human_time_diff($last_cleared) . ' ' . __('ago', 'master-addons')
717 : __('Never', 'master-addons');
718
719 return [
720 'enabled' => $this->is_enabled(),
721 'total_size' => $actual_size,
722 'total_size_formatted' => size_format($actual_size),
723 'total_size_hr' => size_format($actual_size),
724 'file_count' => $actual_count,
725 'cached_pages' => count($global_meta['posts'] ?? []),
726 'post_count' => count($global_meta['posts'] ?? []),
727 'last_cleared' => $last_cleared,
728 'last_cleared_formatted' => $last_cleared_formatted,
729 'cache_path' => $this->cache_path,
730 'cache_url' => $this->cache_url,
731 'cache_directory' => str_replace(ABSPATH, '', $this->cache_path),
732 'posts' => $global_meta['posts'] ?? [],
733 'cached_files' => $cached_files,
734 ];
735 }
736
737 /**
738 * Regenerate cache for all posts with Elementor content
739 */
740 public function regenerate_all_cache()
741 {
742 // Clear existing first
743 $this->clear_all_cache();
744
745 // Find all posts with Elementor data
746 $posts = get_posts([
747 'post_type' => ['page', 'post', 'elementor_library'],
748 'posts_per_page' => -1,
749 'meta_key' => '_elementor_data',
750 'fields' => 'ids',
751 'post_status' => 'publish',
752 ]);
753
754 $count = 0;
755 foreach ($posts as $post_id) {
756 if ($this->generate_post_cache($post_id)) {
757 $count++;
758 }
759 }
760
761 return $count;
762 }
763
764 /**
765 * Database fallback when file writes fail
766 */
767 private function store_in_database($post_id, $css_content, $js_content)
768 {
769 if (!empty($css_content)) {
770 update_post_meta($post_id, '_jltma_cached_css', $css_content);
771 }
772 if (!empty($js_content)) {
773 update_post_meta($post_id, '_jltma_cached_js', $js_content);
774 }
775 update_post_meta($post_id, '_jltma_cache_in_db', true);
776 }
777
778 /**
779 * Simple CSS minification
780 */
781 private function minify_css($css)
782 {
783 // Remove comments
784 $css = preg_replace('/\/\*[^*]*\*+([^\/][^*]*\*+)*\//', '', $css);
785 // Remove whitespace
786 $css = preg_replace('/\s+/', ' ', $css);
787 // Remove space around selectors
788 $css = preg_replace('/\s*([\{\}\;\:\,])\s*/', '$1', $css);
789 return trim($css);
790 }
791
792 /**
793 * Compress content with gzip
794 *
795 * @param string $content Content to compress
796 * @param int $level Compression level (1-9, default 9)
797 * @return string|false Compressed content or false on failure
798 */
799 private function gzip_content($content, $level = 9)
800 {
801 if (!function_exists('gzencode')) {
802 return false;
803 }
804
805 return gzencode($content, $level);
806 }
807
808 /**
809 * Write file with optional gzip version
810 *
811 * @param string $filepath Full path to file
812 * @param string $content File content
813 * @return array ['written' => bool, 'gzip_written' => bool, 'gzip_size' => int]
814 */
815 private function write_with_gzip($filepath, $content)
816 {
817 $result = [
818 'written' => false,
819 'gzip_written' => false,
820 'gzip_size' => 0,
821 ];
822
823 // Write original file
824 $result['written'] = (bool) file_put_contents($filepath, $content);
825
826 if (!$result['written']) {
827 return $result;
828 }
829
830 // Write gzipped version
831 $gzipped = $this->gzip_content($content);
832 if ($gzipped !== false) {
833 $gzip_path = $filepath . '.gz';
834 $result['gzip_written'] = (bool) file_put_contents($gzip_path, $gzipped);
835 if ($result['gzip_written']) {
836 $result['gzip_size'] = strlen($gzipped);
837 }
838 }
839
840 return $result;
841 }
842
843 /**
844 * Check if we're in Elementor editor
845 */
846 private function is_elementor_editor()
847 {
848 if (!class_exists('\Elementor\Plugin')) {
849 return false;
850 }
851
852 $elementor = \Elementor\Plugin::$instance;
853
854 if (!$elementor || !isset($elementor->editor) || !isset($elementor->preview)) {
855 return false;
856 }
857
858 return $elementor->editor->is_edit_mode() || $elementor->preview->is_preview_mode();
859 }
860
861 /**
862 * Handle plugin/theme upgrades
863 */
864 public function on_upgrade_complete($upgrader, $options)
865 {
866 // Clear cache when Master Addons is updated
867 if (
868 $options['action'] === 'update' &&
869 $options['type'] === 'plugin' &&
870 isset($options['plugins']) &&
871 in_array('master-addons/master-addons.php', $options['plugins'])
872 ) {
873 $this->clear_all_cache();
874 }
875 }
876
877 /**
878 * AJAX: Clear all cache
879 */
880 public function ajax_clear_cache()
881 {
882 check_ajax_referer('jltma_admin_nonce', 'nonce');
883
884 if (!current_user_can('manage_options')) {
885 wp_send_json_error(['message' => __('Permission denied', 'master-addons')]);
886 }
887
888 $this->clear_all_cache();
889
890 wp_send_json_success([
891 'message' => __('Cache cleared successfully', 'master-addons'),
892 'stats' => $this->get_cache_stats(),
893 ]);
894 }
895
896 /**
897 * AJAX: Regenerate all cache
898 */
899 public function ajax_regenerate_cache()
900 {
901 check_ajax_referer('jltma_admin_nonce', 'nonce');
902
903 if (!current_user_can('manage_options')) {
904 wp_send_json_error(['message' => __('Permission denied', 'master-addons')]);
905 }
906
907 $count = $this->regenerate_all_cache();
908
909 wp_send_json_success([
910 /* translators: %d: number of pages */
911 'message' => sprintf(__('Regenerated cache for %d pages', 'master-addons'), $count),
912 'stats' => $this->get_cache_stats(),
913 ]);
914 }
915
916 /**
917 * AJAX: Get cache stats
918 */
919 public function ajax_get_cache_stats()
920 {
921 check_ajax_referer('jltma_admin_nonce', 'nonce');
922
923 if (!current_user_can('manage_options')) {
924 wp_send_json_error(['message' => __('Permission denied', 'master-addons')]);
925 }
926
927 wp_send_json_success($this->get_cache_stats());
928 }
929
930 /**
931 * Get cache directory path
932 */
933 public function get_cache_path()
934 {
935 return $this->cache_path;
936 }
937
938 /**
939 * Get cache directory URL
940 */
941 public function get_cache_url()
942 {
943 return $this->cache_url;
944 }
945
946 /**
947 * AJAX: Clear single post cache
948 */
949 public function ajax_clear_single_cache()
950 {
951 check_ajax_referer('jltma_admin_nonce', 'nonce');
952
953 if (!current_user_can('manage_options')) {
954 wp_send_json_error(['message' => __('Permission denied', 'master-addons')]);
955 }
956
957 $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
958
959 if (!$post_id) {
960 wp_send_json_error(['message' => __('Invalid post ID', 'master-addons')]);
961 }
962
963 $this->invalidate_post_cache($post_id);
964
965 wp_send_json_success([
966 /* translators: %d: post ID number */
967 'message' => sprintf(__('Cache cleared for post #%d', 'master-addons'), $post_id),
968 'stats' => $this->get_cache_stats(),
969 ]);
970 }
971
972 /**
973 * AJAX: Regenerate single post cache
974 */
975 public function ajax_regenerate_single_cache()
976 {
977 check_ajax_referer('jltma_admin_nonce', 'nonce');
978
979 if (!current_user_can('manage_options')) {
980 wp_send_json_error(['message' => __('Permission denied', 'master-addons')]);
981 }
982
983 $post_id = isset($_POST['post_id']) ? absint($_POST['post_id']) : 0;
984
985 if (!$post_id) {
986 wp_send_json_error(['message' => __('Invalid post ID', 'master-addons')]);
987 }
988
989 // Clear existing cache first
990 $this->invalidate_post_cache($post_id);
991
992 // Regenerate
993 $result = $this->generate_post_cache($post_id);
994
995 if ($result) {
996 wp_send_json_success([
997 /* translators: %d: post ID number */
998 'message' => sprintf(__('Cache regenerated for post #%d', 'master-addons'), $post_id),
999 'stats' => $this->get_cache_stats(),
1000 ]);
1001 } else {
1002 wp_send_json_error([
1003 /* translators: %d: post ID number */
1004 'message' => sprintf(__('Failed to regenerate cache for post #%d', 'master-addons'), $post_id),
1005 ]);
1006 }
1007 }
1008
1009 /**
1010 * AJAX: Save performance settings
1011 */
1012 public function ajax_save_performance_settings()
1013 {
1014 check_ajax_referer('jltma_performance_settings_nonce_action', '_wpnonce');
1015
1016 if (!current_user_can('manage_options')) {
1017 wp_send_json_error(['message' => __('Permission denied', 'master-addons')]);
1018 }
1019
1020 // Save settings
1021 $dynamic_assets = isset($_POST['jltma_dynamic_assets_enabled']) ? true : false;
1022 $cache_enabled = isset($_POST['jltma_cache_enabled']) ? true : false;
1023 $cache_minify = isset($_POST['jltma_cache_minify']) ? true : false;
1024 $cache_debug = isset($_POST['jltma_cache_debug']) ? true : false;
1025
1026 update_option('jltma_dynamic_assets_enabled', $dynamic_assets);
1027 update_option('jltma_cache_enabled', $cache_enabled);
1028 update_option('jltma_cache_minify', $cache_minify);
1029 update_option('jltma_cache_debug', $cache_debug);
1030
1031 // Clear cache if caching was disabled
1032 if (!$cache_enabled) {
1033 $this->clear_all_cache();
1034 }
1035
1036 wp_send_json_success([
1037 'message' => __('Performance settings saved', 'master-addons'),
1038 ]);
1039 }
1040 }
1041