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 / template-library-cache.php

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

1,225 lines 40.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace MasterAddons\Inc\Classes;
4
5 use MasterAddons\Inc\Admin\Templates;
6
7 if (!defined('ABSPATH')) {
8 exit;
9 }
10
11 // phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.WP.AlternativeFunctions.file_system_operations_is_writable, WordPress.WP.AlternativeFunctions.rename_rename -- Local template-library cache directory housekeeping (under wp-content/uploads); native PHP filesystem calls are appropriate here and WP_Filesystem credential prompts are not desirable for background cache operations.
12
13 /**
14 * Template Library Cache
15 * Provides file-based caching with scheduled updates for template import functionality
16 */
17 class Template_Library_Cache
18 {
19
20 private static $instance = null;
21 private $cache_dir;
22 private $cache_expiry;
23 private $config;
24
25 public function __construct()
26 {
27 $upload_dir = wp_upload_dir();
28 $basedir = !empty($upload_dir['basedir']) ? $upload_dir['basedir'] : '';
29 $this->cache_dir = $basedir . '/master_addons/templates-library/';
30 $this->cache_expiry = apply_filters('jltma_cache_expiry', 6 * HOUR_IN_SECONDS); // 6 hours default, filterable
31
32 // Initialize config safely
33 add_action('init', [$this, 'init_config'], 20);
34 add_action('init', [$this, 'init'], 25);
35 }
36
37 public function init_config()
38 {
39 // Initialize config after templates system is ready
40 if (function_exists('MasterAddons\\Inc\\Admin\\Templates\\master_addons_templates')) {
41 $templates_instance = Templates\master_addons_templates();
42 if ($templates_instance && isset($templates_instance->config)) {
43 $this->config = $templates_instance->config->get('api');
44 }
45 }
46
47 }
48
49 public function init()
50 {
51 // Clean up old incorrect template-kits folder if it exists
52 $this->cleanup_incorrect_folders();
53
54 // Ensure cache directory exists
55 $this->ensure_cache_directory();
56
57 // Schedule cache updates
58 add_action('wp', [$this, 'schedule_cache_updates']);
59 add_action('jltma_templates_cache_update', [$this, 'update_templates_cache']);
60
61 // Admin hooks
62 add_action('admin_init', [$this, 'maybe_clear_cache']);
63
64 // Performance optimizations
65 add_action('wp_ajax_jltma_preload_cache', [$this, 'preload_cache_ajax']);
66 add_action('jltma_templates_preload_cache', [$this, 'preload_popular_templates']);
67 add_action('jltma_background_preload', [$this, 'do_background_preload']);
68
69 // Extend existing cache methods
70 add_filter('jltma_templates_cache_enabled', '__return_true');
71 }
72
73 /**
74 * Ensure cache directory exists with proper structure
75 */
76 private function ensure_cache_directory()
77 {
78 // Check if uploads directory is writable
79 if (!$this->is_uploads_writable()) {
80 return false;
81 }
82
83 if (!file_exists($this->cache_dir)) {
84 if (!wp_mkdir_p($this->cache_dir)) {
85 return false;
86 }
87
88 // Create subdirectories for different template types
89 // Removed 'template-kits' as it should be in its own separate folder
90 $subdirs = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
91 foreach ($subdirs as $subdir) {
92 wp_mkdir_p($this->cache_dir . $subdir . '/');
93 wp_mkdir_p($this->cache_dir . $subdir . '/categories/');
94 wp_mkdir_p($this->cache_dir . $subdir . '/keywords/');
95 wp_mkdir_p($this->cache_dir . $subdir . '/templates/');
96 wp_mkdir_p($this->cache_dir . $subdir . '/images/');
97 }
98
99 // Create .htaccess for security
100 $htaccess_content = "Options -Indexes\n<Files \"*.json\">\nOrder allow,deny\nAllow from all\n</Files>";
101 $this->fs_put_contents($this->cache_dir . '.htaccess', $htaccess_content);
102
103 // Create index.php files
104 $index_content = "<?php\n// Silence is golden.\n";
105 $this->fs_put_contents($this->cache_dir . 'index.php', $index_content);
106
107 foreach ($subdirs as $subdir) {
108 $this->fs_put_contents($this->cache_dir . $subdir . '/index.php', $index_content);
109 }
110 }
111
112 return true;
113 }
114
115 /**
116 * Clean up incorrect folders created in wrong location
117 */
118 private function cleanup_incorrect_folders()
119 {
120 // Remove template-kits folder from templates-library if it exists
121 $incorrect_folder = $this->cache_dir . 'template-kits/';
122 if (file_exists($incorrect_folder)) {
123 $this->delete_directory_recursively($incorrect_folder);
124 }
125
126 // Also check for any variations that might have been created
127 $variations = ['template_kits', 'templatekits', 'template-kit'];
128 foreach ($variations as $variant) {
129 $incorrect_variant = $this->cache_dir . $variant . '/';
130 if (file_exists($incorrect_variant)) {
131 $this->delete_directory_recursively($incorrect_variant);
132 }
133 }
134 }
135
136 /**
137 * Delete a directory and all its contents recursively
138 */
139 private function delete_directory_recursively($dir)
140 {
141 if (!file_exists($dir)) {
142 return;
143 }
144
145 if (!is_dir($dir)) {
146 return;
147 }
148
149 // Normalize the directory path to avoid double slashes
150 $dir = rtrim($dir, '/\\');
151
152 // Try to scan the directory, but handle failures gracefully
153 $scan_result = @scandir($dir);
154 if ($scan_result === false) {
155 // If we can't scan it, try to remove it directly
156 @rmdir($dir);
157 return;
158 }
159
160 $files = array_diff($scan_result, array('.', '..'));
161 foreach ($files as $file) {
162 $path = $dir . DIRECTORY_SEPARATOR . $file;
163 if (is_dir($path)) {
164 $this->delete_directory_recursively($path);
165 } else {
166 wp_delete_file($path);
167 }
168 }
169 @rmdir($dir);
170 }
171
172 /**
173 * Check if uploads directory is writable
174 */
175 private function is_uploads_writable()
176 {
177 $upload_dir = wp_upload_dir();
178
179 // Check if uploads dir exists and is writable
180 if (!file_exists($upload_dir['basedir'])) {
181 return false;
182 }
183
184 return is_writable($upload_dir['basedir']);
185 }
186
187 /**
188 * Schedule cache update events via Background_Task_Manager
189 */
190 public function schedule_cache_updates()
191 {
192 Background_Task_Manager::get_instance()->schedule_recurring(
193 'jltma_templates_cache_update',
194 12 * HOUR_IN_SECONDS
195 );
196 }
197
198 /**
199 * Get cached templates for specific tab
200 */
201 public function get_cached_templates($tab, $force_refresh = false)
202 {
203 // Try transient cache first if file cache is not available
204 if (!$this->is_file_cache_available()) {
205 return $this->get_transient_cached_templates($tab, $force_refresh);
206 }
207
208 $cache_file = $this->cache_dir . "{$tab}/templates/templates.json";
209 $cache_meta_file = $this->cache_dir . "{$tab}/templates/meta.json";
210
211 // Check if cache exists and is valid
212 if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) {
213 $cached_data = $this->read_cache_file($cache_file);
214 if ($cached_data !== false) {
215 // Update thumbnail URLs to use cache folder first
216 foreach ($cached_data as &$template) {
217 $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']);
218 if ($cached_thumbnail) {
219 $template['thumbnail'] = $cached_thumbnail;
220 }
221 }
222 return $cached_data;
223 }
224 }
225
226 // Fetch fresh data from remote API
227 $fresh_data = $this->fetch_remote_templates($tab);
228
229 if ($fresh_data !== false) {
230 // Update thumbnail URLs to use cache folder first
231 foreach ($fresh_data as &$template) {
232 $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']);
233 if ($cached_thumbnail) {
234 $template['thumbnail'] = $cached_thumbnail;
235 }
236 }
237
238 // Cache the data
239 $this->write_cache_file($cache_file, $fresh_data);
240 $this->write_cache_meta($cache_meta_file);
241
242 // Cache individual template thumbnails
243 $this->cache_template_images($fresh_data, $tab);
244
245 return $fresh_data;
246 }
247
248 // Fallback to expired cache if available
249 $fallback_data = $this->read_cache_file($cache_file);
250 if ($fallback_data !== false) {
251 // Update thumbnail URLs to use cache folder first for fallback data
252 foreach ($fallback_data as &$template) {
253 $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']);
254 if ($cached_thumbnail) {
255 $template['thumbnail'] = $cached_thumbnail;
256 }
257 }
258 }
259 return $fallback_data;
260 }
261
262 /**
263 * Get cached categories for specific tab
264 */
265 public function get_cached_categories($tab, $force_refresh = false)
266 {
267 // Try transient cache first if file cache is not available
268 if (!$this->is_file_cache_available()) {
269 return $this->get_transient_cached_categories($tab, $force_refresh);
270 }
271
272 $cache_file = $this->cache_dir . "{$tab}/categories/categories.json";
273 $cache_meta_file = $this->cache_dir . "{$tab}/categories/meta.json";
274
275 if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) {
276 $cached_data = $this->read_cache_file($cache_file);
277 if ($cached_data !== false) {
278 return $cached_data;
279 }
280 }
281
282 $fresh_data = $this->fetch_remote_categories($tab);
283
284 if ($fresh_data !== false) {
285 $this->write_cache_file($cache_file, $fresh_data);
286 $this->write_cache_meta($cache_meta_file);
287 return $fresh_data;
288 }
289
290 return $this->read_cache_file($cache_file);
291 }
292
293 /**
294 * Get cached keywords for specific tab
295 */
296 public function get_cached_keywords($tab, $force_refresh = false)
297 {
298 // Try transient cache first if file cache is not available
299 if (!$this->is_file_cache_available()) {
300 return $this->get_transient_cached_keywords($tab, $force_refresh);
301 }
302
303 $cache_file = $this->cache_dir . "{$tab}/keywords/keywords.json";
304 $cache_meta_file = $this->cache_dir . "{$tab}/keywords/meta.json";
305
306 if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) {
307 $cached_data = $this->read_cache_file($cache_file);
308 if ($cached_data !== false) {
309 return $cached_data;
310 }
311 }
312
313 $fresh_data = $this->fetch_remote_keywords($tab);
314
315 if ($fresh_data !== false) {
316 $this->write_cache_file($cache_file, $fresh_data);
317 $this->write_cache_meta($cache_meta_file);
318 return $fresh_data;
319 }
320
321 return $this->read_cache_file($cache_file);
322 }
323
324 /**
325 * Get cached individual template
326 */
327 public function get_cached_template($template_id, $tab, $force_refresh = false)
328 {
329 $cache_file = $this->cache_dir . "{$tab}/templates/template-{$template_id}.json";
330 $cache_meta_file = $this->cache_dir . "{$tab}/templates/template-{$template_id}-meta.json";
331
332 if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) {
333 $cached_data = $this->read_cache_file($cache_file);
334 if ($cached_data !== false) {
335 return $cached_data;
336 }
337 }
338
339 // For individual templates, we don't cache them unless they're part of a larger fetch
340 // This prevents excessive API calls for single template requests
341 return false;
342 }
343
344 /**
345 * Cache individual template data (called after successful API fetch)
346 */
347 public function cache_template_data($template_id, $tab, $data)
348 {
349 $cache_file = $this->cache_dir . "{$tab}/templates/template-{$template_id}.json";
350 $cache_meta_file = $this->cache_dir . "{$tab}/templates/template-{$template_id}-meta.json";
351
352 $this->write_cache_file($cache_file, $data);
353 $this->write_cache_meta($cache_meta_file);
354 }
355
356 /**
357 * Fetch templates from remote API
358 */
359 private function fetch_remote_templates($tab)
360 {
361 if (empty($this->config)) {
362 return false;
363 }
364
365 $api_url = $this->config['base'] . $this->config['path'] . $this->config['endpoints']['templates'] . $tab;
366
367 $response = wp_remote_get($api_url, [
368 'timeout' => 60,
369 'sslverify' => false,
370 'headers' => [
371 'User-Agent' => 'Master Addons Templates Cache/' . JLTMA_VER
372 ]
373 ]);
374 if (is_wp_error($response)) {
375 return false;
376 }
377
378 $body = wp_remote_retrieve_body($response);
379 $data = json_decode($body, true);
380
381 if (json_last_error() !== JSON_ERROR_NONE || !isset($data['success']) || !$data['success']) {
382 return false;
383 }
384
385 return isset($data['templates']) ? $data['templates'] : [];
386 }
387
388 /**
389 * Fetch categories from remote API
390 */
391 private function fetch_remote_categories($tab)
392 {
393 if (empty($this->config)) {
394 return false;
395 }
396
397 $api_url = $this->config['base'] . $this->config['path'] . $this->config['endpoints']['categories'] . $tab;
398
399 $response = wp_remote_get($api_url, [
400 'timeout' => 60,
401 'sslverify' => false
402 ]);
403
404 if (is_wp_error($response)) {
405 return false;
406 }
407
408 $body = wp_remote_retrieve_body($response);
409 $data = json_decode($body, true);
410
411 if (json_last_error() !== JSON_ERROR_NONE || !isset($data['success']) || !$data['success']) {
412 return false;
413 }
414
415 return isset($data['terms']) ? $data['terms'] : [];
416 }
417
418 /**
419 * Fetch keywords from remote API
420 */
421 private function fetch_remote_keywords($tab)
422 {
423 if (empty($this->config)) {
424 return false;
425 }
426
427 $api_url = $this->config['base'] . $this->config['path'] . $this->config['endpoints']['keywords'] . $tab;
428
429 $response = wp_remote_get($api_url, [
430 'timeout' => 60,
431 'sslverify' => false
432 ]);
433
434 if (is_wp_error($response)) {
435 return false;
436 }
437
438 $body = wp_remote_retrieve_body($response);
439 $data = json_decode($body, true);
440
441 if (json_last_error() !== JSON_ERROR_NONE || !isset($data['success']) || !$data['success']) {
442 return false;
443 }
444
445 return isset($data['terms']) ? $data['terms'] : [];
446 }
447
448 /**
449 * Cache template images locally
450 */
451 private function cache_template_images($templates, $tab)
452 {
453 if (!is_array($templates)) {
454 return;
455 }
456
457 foreach ($templates as $template) {
458 if (isset($template['thumbnail']) && !empty($template['thumbnail'])) {
459 $template_id = $template['template_id'] ?? uniqid();
460 $this->cache_image($template['thumbnail'], $tab, "template-{$template_id}-thumb");
461 }
462
463 if (isset($template['preview']) && !empty($template['preview'])) {
464 $template_id = $template['template_id'] ?? uniqid();
465 $this->cache_image($template['preview'], $tab, "template-{$template_id}-preview");
466 }
467 }
468 }
469
470 /**
471 * Cache individual image
472 */
473 private function cache_image($image_url, $tab, $filename)
474 {
475 if (empty($image_url)) {
476 return false;
477 }
478
479 $extension = pathinfo($image_url, PATHINFO_EXTENSION);
480 if (empty($extension)) {
481 $extension = 'jpg';
482 }
483
484 $local_file = $this->cache_dir . "{$tab}/images/{$filename}.{$extension}";
485
486 // Skip if already cached and recent
487 if (file_exists($local_file) && (time() - filemtime($local_file)) < DAY_IN_SECONDS) {
488 return $local_file;
489 }
490
491 // Ensure the directory exists before trying to write
492 $image_dir = dirname($local_file);
493 if (!file_exists($image_dir)) {
494 wp_mkdir_p($image_dir);
495 }
496
497 $response = wp_remote_get($image_url, [
498 'timeout' => 30
499 ]);
500
501 if (is_wp_error($response)) {
502 return false;
503 }
504
505 $image_data = wp_remote_retrieve_body($response);
506
507 if ($this->fs_put_contents($local_file, $image_data)) {
508 return $local_file;
509 }
510
511 return false;
512 }
513
514 /**
515 * Update templates cache (scheduled event)
516 */
517 public function update_templates_cache()
518 {
519 $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
520 $btm = Background_Task_Manager::get_instance();
521
522 foreach ($template_types as $tab) {
523 try {
524 $btm->execute_with_retry(function () use ($tab) {
525 // Update templates
526 $this->get_cached_templates($tab, true);
527 // Update categories
528 $this->get_cached_categories($tab, true);
529 // Update keywords
530 $this->get_cached_keywords($tab, true);
531 }, 3, "templates_cache_sync_{$tab}");
532 } catch (\Exception $e) {
533 // Individual tab failure logged by execute_with_retry; continue with others
534 }
535 }
536
537 // Clean up old cache files
538 $this->cleanup_old_cache();
539
540 // Update last cache time
541 set_transient('jltma_templates_last_cache_update', time(), DAY_IN_SECONDS);
542 }
543
544 /**
545 * Check if cache is valid
546 */
547 private function is_cache_valid($meta_file)
548 {
549 if (!file_exists($meta_file)) {
550 return false;
551 }
552
553 $meta = json_decode(file_get_contents($meta_file), true);
554 if (!$meta || !isset($meta['timestamp'])) {
555 return false;
556 }
557
558 return (time() - $meta['timestamp']) < $this->cache_expiry;
559 }
560
561 /**
562 * Read cache file with priority tracking
563 */
564 private function read_cache_file($file_path)
565 {
566 if (!file_exists($file_path)) {
567 return false;
568 }
569
570 // Track access for priority system
571 $this->track_cache_access($file_path);
572
573 $content = file_get_contents($file_path);
574 if ($content === false) {
575 return false;
576 }
577
578 $data = json_decode($content, true);
579 return json_last_error() === JSON_ERROR_NONE ? $data : false;
580 }
581
582 /**
583 * Write a file through the WP_Filesystem API.
584 *
585 * @param string $file
586 * @param string $contents
587 * @return bool
588 */
589 private function fs_put_contents($file, $contents)
590 {
591 global $wp_filesystem;
592 if (empty($wp_filesystem)) {
593 require_once ABSPATH . 'wp-admin/includes/file.php';
594 WP_Filesystem();
595 }
596 if (empty($wp_filesystem)) {
597 return false;
598 }
599 return $wp_filesystem->put_contents($file, $contents, FS_CHMOD_FILE);
600 }
601
602 /**
603 * Write cache file
604 */
605 private function write_cache_file($file_path, $data)
606 {
607 $dir = dirname($file_path);
608 if (!file_exists($dir)) {
609 wp_mkdir_p($dir);
610 }
611
612 $json = wp_json_encode($data, JSON_PRETTY_PRINT);
613 return $this->fs_put_contents($file_path, $json) !== false;
614 }
615
616 /**
617 * Write cache metadata
618 */
619 private function write_cache_meta($meta_file)
620 {
621 $meta = [
622 'timestamp' => time(),
623 'version' => JLTMA_VER,
624 'expiry' => $this->cache_expiry
625 ];
626
627 return $this->fs_put_contents($meta_file, wp_json_encode($meta)) !== false;
628 }
629
630 /**
631 * Clear all template cache
632 */
633 public function clear_cache()
634 {
635 $cleared = false;
636
637 // Clear file cache if available
638 if (file_exists($this->cache_dir)) {
639 $this->delete_directory_contents($this->cache_dir);
640 $this->ensure_cache_directory();
641 $cleared = true;
642 }
643
644 // Always clear transient cache
645 $this->clear_transient_cache();
646
647 // Clear related transients (legacy support)
648 $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
649 foreach ($template_types as $tab) {
650 delete_transient("master_addons_templates_master-api_{$tab}");
651 delete_transient("master_addons_categories_master-api_{$tab}");
652 delete_transient("master_addons_keywords_master-api_{$tab}");
653 }
654
655 delete_transient('jltma_templates_last_cache_update');
656
657 return true;
658 }
659
660 /**
661 * Refresh cache by clearing and fetching fresh data from API
662 */
663 public function refresh_cache()
664 {
665 // Clear all existing cache
666 $this->clear_cache();
667
668 // Force fetch fresh data from API for all template types
669 $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
670 $refreshed_data = [];
671
672 foreach ($template_types as $tab) {
673 // Force refresh templates
674 $templates = $this->get_cached_templates($tab, true);
675
676 // Force refresh categories
677 $categories = $this->get_cached_categories($tab, true);
678
679 // Force refresh keywords
680 $keywords = $this->get_cached_keywords($tab, true);
681
682 $refreshed_data[$tab] = [
683 'templates' => is_array($templates) ? count($templates) : 0,
684 'categories' => is_array($categories) ? count($categories) : 0,
685 'keywords' => is_array($keywords) ? count($keywords) : 0
686 ];
687 }
688
689 // Update last cache refresh time
690 set_transient('jltma_templates_last_cache_update', time(), DAY_IN_SECONDS);
691
692 // Log successful refresh
693
694 return $refreshed_data;
695 }
696
697 /**
698 * Clean up old cache files with priority system
699 */
700 private function cleanup_old_cache()
701 {
702 $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
703
704 // Cache priority ages (high priority files are kept longer)
705 $priority_ages = [
706 'high' => 14 * DAY_IN_SECONDS, // 14 days for high priority
707 'medium' => 7 * DAY_IN_SECONDS, // 7 days for medium priority
708 'low' => 3 * DAY_IN_SECONDS // 3 days for low priority
709 ];
710
711 foreach ($template_types as $tab) {
712 $tab_dir = $this->cache_dir . $tab . '/';
713 if (!file_exists($tab_dir)) {
714 continue;
715 }
716
717 $subdirs = ['categories', 'keywords', 'templates', 'images'];
718 foreach ($subdirs as $subdir) {
719 $full_dir = $tab_dir . $subdir . '/';
720 if (!file_exists($full_dir)) {
721 continue;
722 }
723
724 $files = glob($full_dir . '*');
725 foreach ($files as $file) {
726 if (is_file($file)) {
727 $file_age = time() - filemtime($file);
728 $priority = $this->get_file_cache_priority($file, $tab);
729 $max_age = $priority_ages[$priority] ?? $priority_ages['low'];
730
731 if ($file_age > $max_age) {
732 wp_delete_file($file);
733 }
734 }
735 }
736 }
737 }
738 }
739
740 /**
741 * Determine cache file priority based on usage patterns
742 */
743 private function get_file_cache_priority($file_path, $tab)
744 {
745 $filename = basename($file_path);
746 $access_count = $this->get_file_access_count($file_path);
747 $recent_access = $this->get_recent_access_time($file_path);
748
749 // High priority: Frequently accessed files (>10 times) or recently accessed (within 2 days)
750 if ($access_count > 10 || (time() - $recent_access) < (2 * DAY_IN_SECONDS)) {
751 return 'high';
752 }
753
754 // Medium priority: Moderately accessed files (3-10 times) or accessed within a week
755 if ($access_count >= 3 || (time() - $recent_access) < (7 * DAY_IN_SECONDS)) {
756 return 'medium';
757 }
758
759 // Low priority: Everything else
760 return 'low';
761 }
762
763 /**
764 * Get file access count from usage tracking
765 */
766 private function get_file_access_count($file_path)
767 {
768 $access_data = get_transient('jltma_cache_access_' . md5($file_path));
769 return $access_data ? (int) $access_data['count'] : 0;
770 }
771
772 /**
773 * Get recent access time for file
774 */
775 private function get_recent_access_time($file_path)
776 {
777 $access_data = get_transient('jltma_cache_access_' . md5($file_path));
778 return $access_data ? (int) $access_data['last_access'] : filemtime($file_path);
779 }
780
781 /**
782 * Track cache file access for priority system
783 */
784 private function track_cache_access($file_path)
785 {
786 $access_key = 'jltma_cache_access_' . md5($file_path);
787 $access_data = get_transient($access_key) ?: ['count' => 0, 'last_access' => 0];
788
789 $access_data['count']++;
790 $access_data['last_access'] = time();
791
792 set_transient($access_key, $access_data, 30 * DAY_IN_SECONDS);
793 }
794
795 /**
796 * Delete directory contents recursively
797 */
798 private function delete_directory_contents($dir)
799 {
800 if (!file_exists($dir)) {
801 return;
802 }
803
804 $files = glob($dir . '*', GLOB_MARK);
805 foreach ($files as $file) {
806 if (is_dir($file)) {
807 $this->delete_directory_contents($file);
808 rmdir($file);
809 } else {
810 wp_delete_file($file);
811 }
812 }
813 }
814
815 /**
816 * Handle cache clearing from admin
817 */
818 public function maybe_clear_cache()
819 {
820 if (isset($_GET['jltma_clear_templates_cache']) &&
821 isset($_GET['_wpnonce']) &&
822 wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'jltma_clear_templates_cache') &&
823 current_user_can('manage_options')) {
824
825 $this->clear_cache();
826
827 wp_safe_redirect(add_query_arg([
828 'jltma_templates_cache_cleared' => '1'
829 ], remove_query_arg(['jltma_clear_templates_cache', '_wpnonce'])));
830 exit;
831 }
832
833 // Handle cache refresh from admin
834 if (isset($_GET['jltma_refresh_templates_cache']) &&
835 isset($_GET['_wpnonce']) &&
836 wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'jltma_refresh_templates_cache') &&
837 current_user_can('manage_options')) {
838
839 $this->refresh_cache();
840
841 wp_safe_redirect(add_query_arg([
842 'jltma_templates_cache_refreshed' => '1'
843 ], remove_query_arg(['jltma_refresh_templates_cache', '_wpnonce'])));
844 exit;
845 }
846 }
847
848 /**
849 * Get cached image URL
850 */
851 public function get_cached_image_url($original_url, $tab, $filename)
852 {
853 $extension = pathinfo($original_url, PATHINFO_EXTENSION) ?: 'jpg';
854 $local_file = $this->cache_dir . "{$tab}/images/{$filename}.{$extension}";
855
856 if (file_exists($local_file)) {
857 $upload_dir = wp_upload_dir();
858 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
859 return $upload_dir['baseurl'] . $relative_path;
860 }
861
862 return $original_url;
863 }
864
865 /**
866 * Get template kit thumbnail from cache or generate fallback URL
867 */
868 public function get_kit_thumbnail_url($kit_name, $template_name = 'home', $original_url = null)
869 {
870 // Normalize kit name for filename
871 $kit_slug = sanitize_title($kit_name);
872 $template_slug = sanitize_title($template_name);
873
874 // Check cache directory first (templates-library images)
875 $cache_image_dir = $this->cache_dir . 'master_section/images/';
876 $cached_file_patterns = [
877 "{$kit_slug}-{$template_slug}.jpg",
878 "{$kit_slug}-{$template_slug}.png",
879 "{$kit_slug}.jpg",
880 "{$kit_slug}.png"
881 ];
882
883 foreach ($cached_file_patterns as $pattern) {
884 $cached_file = $cache_image_dir . $pattern;
885 if (file_exists($cached_file)) {
886 $upload_dir = wp_upload_dir();
887 $relative_path = str_replace($upload_dir['basedir'], '', $cached_file);
888 return $upload_dir['baseurl'] . $relative_path;
889 }
890 }
891
892 // If original URL provided, return it
893 if ($original_url) {
894 return $original_url;
895 }
896
897 // Generate expected thumbnail URL from master-addons.com
898 $kit_version = $this->get_kit_version($kit_name);
899 return "https://master-addons.com/templates-kit/{$kit_slug}{$kit_version}/{$template_slug}.jpg";
900 }
901
902 /**
903 * Get kit version suffix for URL generation
904 */
905 private function get_kit_version($kit_name)
906 {
907 // Common version patterns for kits
908 $version_patterns = [
909 'business-agency' => '-v1',
910 'restaurant' => '-v2',
911 'portfolio' => '-v1',
912 'ecommerce' => '-v3'
913 ];
914
915 $kit_slug = sanitize_title($kit_name);
916 return $version_patterns[$kit_slug] ?? '-v1';
917 }
918
919 /**
920 * Get cache statistics
921 */
922 public function get_cache_stats()
923 {
924 $stats = [
925 'cache_dir_exists' => file_exists($this->cache_dir),
926 'cache_size' => $this->get_directory_size($this->cache_dir),
927 'last_update' => get_transient('jltma_templates_last_cache_update'),
928 'template_types' => [],
929 'next_scheduled_update' => wp_next_scheduled('jltma_templates_cache_update'),
930 'total_kits' => 0,
931 'total_templates' => 0
932 ];
933
934 $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
935 $total_templates = 0;
936
937 foreach ($template_types as $type) {
938 $template_count = 0;
939 $image_count = 0;
940
941 // Count file cache if available
942 if ($this->is_file_cache_available()) {
943 $template_count = count(glob($this->cache_dir . "{$type}/templates/template-*.json"));
944 $image_count = count(glob($this->cache_dir . "{$type}/images/*"));
945 } else {
946 // Count transient cache
947 $cached_templates = get_transient("jltma_templates_{$type}");
948 if ($cached_templates && is_array($cached_templates)) {
949 $template_count = count($cached_templates);
950 }
951 }
952
953 $stats['template_types'][$type] = [
954 'templates' => $template_count,
955 'images' => $image_count
956 ];
957
958 $total_templates += $template_count;
959 }
960
961 // For template kits, we'll count unique kits from cached data
962 $stats['total_kits'] = $this->count_cached_kits();
963 $stats['total_templates'] = $total_templates;
964
965 return $stats;
966 }
967
968 /**
969 * Count cached template kits
970 */
971 private function count_cached_kits()
972 {
973 $kit_count = 0;
974
975 // If using file cache, look for kit manifest files
976 if ($this->is_file_cache_available()) {
977 $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
978 foreach ($template_types as $type) {
979 $templates_file = $this->cache_dir . "{$type}/templates/templates.json";
980 if (file_exists($templates_file)) {
981 $templates_data = $this->read_cache_file($templates_file);
982 if ($templates_data && is_array($templates_data)) {
983 $kit_count += count($templates_data);
984 }
985 }
986 }
987 } else {
988 // Count from transients
989 $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
990 foreach ($template_types as $type) {
991 $cached_templates = get_transient("jltma_templates_{$type}");
992 if ($cached_templates && is_array($cached_templates)) {
993 $kit_count += count($cached_templates);
994 }
995 }
996 }
997
998 return $kit_count;
999 }
1000
1001 /**
1002 * Get directory size in bytes
1003 */
1004 private function get_directory_size($dir)
1005 {
1006 $size = 0;
1007 if (file_exists($dir)) {
1008 foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)) as $file) {
1009 if ($file->isFile()) {
1010 $size += $file->getSize();
1011 }
1012 }
1013 }
1014 return $size;
1015 }
1016
1017 /**
1018 * Check if file cache is available
1019 */
1020 private function is_file_cache_available()
1021 {
1022 return file_exists($this->cache_dir) && is_writable($this->cache_dir);
1023 }
1024
1025 /**
1026 * Get cached templates using transients (fallback method)
1027 */
1028 private function get_transient_cached_templates($tab, $force_refresh = false)
1029 {
1030 $transient_key = "jltma_templates_{$tab}";
1031 $meta_transient_key = "jltma_templates_{$tab}_meta";
1032
1033 // Check if cache exists and is valid
1034 if (!$force_refresh) {
1035 $cached_meta = get_transient($meta_transient_key);
1036 if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) {
1037 $cached_data = get_transient($transient_key);
1038 if ($cached_data !== false) {
1039 // Update thumbnail URLs to use cache folder first for transient cached templates
1040 foreach ($cached_data as &$template) {
1041 $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']);
1042 if ($cached_thumbnail) {
1043 $template['thumbnail'] = $cached_thumbnail;
1044 }
1045 }
1046 return $cached_data;
1047 }
1048 }
1049 }
1050
1051 // Fetch fresh data from remote API
1052 $fresh_data = $this->fetch_remote_templates($tab);
1053
1054 if ($fresh_data !== false) {
1055 // Update thumbnail URLs to use cache folder first for fresh transient templates
1056 foreach ($fresh_data as &$template) {
1057 $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']);
1058 if ($cached_thumbnail) {
1059 $template['thumbnail'] = $cached_thumbnail;
1060 }
1061 }
1062
1063 // Cache the data using transients
1064 set_transient($transient_key, $fresh_data, $this->cache_expiry);
1065 set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry);
1066
1067 return $fresh_data;
1068 }
1069
1070 // Return cached data even if expired
1071 $fallback_transient_data = get_transient($transient_key);
1072 if ($fallback_transient_data !== false) {
1073 // Update thumbnail URLs to use cache folder first for expired transient templates
1074 foreach ($fallback_transient_data as &$template) {
1075 $cached_thumbnail = $this->get_kit_thumbnail_url('', $template['title'], $template['thumbnail']);
1076 if ($cached_thumbnail) {
1077 $template['thumbnail'] = $cached_thumbnail;
1078 }
1079 }
1080 }
1081 return $fallback_transient_data;
1082 }
1083
1084 /**
1085 * Get cached categories using transients (fallback method)
1086 */
1087 private function get_transient_cached_categories($tab, $force_refresh = false)
1088 {
1089 $transient_key = "jltma_categories_{$tab}";
1090 $meta_transient_key = "jltma_categories_{$tab}_meta";
1091
1092 if (!$force_refresh) {
1093 $cached_meta = get_transient($meta_transient_key);
1094 if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) {
1095 $cached_data = get_transient($transient_key);
1096 if ($cached_data !== false) {
1097 return $cached_data;
1098 }
1099 }
1100 }
1101
1102 $fresh_data = $this->fetch_remote_categories($tab);
1103
1104 if ($fresh_data !== false) {
1105 set_transient($transient_key, $fresh_data, $this->cache_expiry);
1106 set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry);
1107 return $fresh_data;
1108 }
1109
1110 return get_transient($transient_key);
1111 }
1112
1113 /**
1114 * Get cached keywords using transients (fallback method)
1115 */
1116 private function get_transient_cached_keywords($tab, $force_refresh = false)
1117 {
1118 $transient_key = "jltma_keywords_{$tab}";
1119 $meta_transient_key = "jltma_keywords_{$tab}_meta";
1120
1121 if (!$force_refresh) {
1122 $cached_meta = get_transient($meta_transient_key);
1123 if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) {
1124 $cached_data = get_transient($transient_key);
1125 if ($cached_data !== false) {
1126 return $cached_data;
1127 }
1128 }
1129 }
1130
1131 $fresh_data = $this->fetch_remote_keywords($tab);
1132
1133 if ($fresh_data !== false) {
1134 set_transient($transient_key, $fresh_data, $this->cache_expiry);
1135 set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry);
1136 return $fresh_data;
1137 }
1138
1139 return get_transient($transient_key);
1140 }
1141
1142 /**
1143 * Clear transient cache (fallback method)
1144 */
1145 private function clear_transient_cache()
1146 {
1147 $template_types = ['master_section', 'master_pages', 'master_popups', 'master_headers', 'master_footers'];
1148
1149 foreach ($template_types as $tab) {
1150 delete_transient("jltma_templates_{$tab}");
1151 delete_transient("jltma_templates_{$tab}_meta");
1152 delete_transient("jltma_categories_{$tab}");
1153 delete_transient("jltma_categories_{$tab}_meta");
1154 delete_transient("jltma_keywords_{$tab}");
1155 delete_transient("jltma_keywords_{$tab}_meta");
1156 }
1157 }
1158
1159 /**
1160 * Preload popular templates in background
1161 */
1162 public function preload_popular_templates()
1163 {
1164 $popular_tabs = ['master_section', 'master_headers'];
1165
1166 foreach ($popular_tabs as $tab) {
1167 if (!get_transient("jltma_preload_{$tab}")) {
1168 wp_schedule_single_event(time() + 60, 'jltma_background_preload', [$tab]);
1169 set_transient("jltma_preload_{$tab}", true, HOUR_IN_SECONDS);
1170 }
1171 }
1172 }
1173
1174 /**
1175 * Background preload handler (callback for jltma_background_preload action)
1176 *
1177 * @param string $tab Template tab to preload
1178 */
1179 public function do_background_preload($tab)
1180 {
1181 if (!empty($tab)) {
1182 $this->get_cached_templates($tab, true);
1183 }
1184 }
1185
1186 /**
1187 * AJAX handler for cache preloading
1188 */
1189 public function preload_cache_ajax()
1190 {
1191 if (!current_user_can('manage_options')) {
1192 wp_die(-1);
1193 }
1194
1195 if (!check_ajax_referer('jltma_preload_cache_nonce', 'security', false)) {
1196 wp_die(-1);
1197 }
1198
1199 $tab = sanitize_text_field( wp_unslash( $_POST['tab'] ?? '' ) );
1200
1201 if (empty($tab)) {
1202 wp_send_json_error('Invalid tab');
1203 }
1204
1205 // Preload in background
1206 $this->get_cached_templates($tab, true);
1207
1208 wp_send_json_success('Cache preloaded for ' . $tab);
1209 }
1210
1211 /**
1212 * Get singleton instance
1213 */
1214 public static function get_instance()
1215 {
1216 if (self::$instance === null) {
1217 self::$instance = new self();
1218 }
1219 return self::$instance;
1220 }
1221 }
1222
1223 // Initialize templates cache manager
1224 Template_Library_Cache::get_instance();
1225