PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / trunk
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits vtrunk
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 trunk, at inc/classes/cache-manager.php

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