PluginProbe
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits / 3.0.9
Master Addons for Elementor – Elementor Addons, Widgets, Mega Menu Builder, Popup Builder, Widget Builder & Template Kits v3.0.9
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-kit-cache.php

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

3,276 lines 118.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 /**
12 * Template Kit Cache
13 * Provides file-based caching for Template Kit functionality (Site Importer)
14 */
15 class Template_Kit_Cache
16 {
17
18 private static $instance = null;
19 private $cache_dir;
20 private $cache_expiry;
21 private $is_pro_enabled;
22 private $purchased_dir;
23
24 public function __construct()
25 {
26 $this->is_pro_enabled = Helper::jltma_premium();
27 $upload_dir = wp_upload_dir();
28 $basedir = !empty($upload_dir['basedir']) ? $upload_dir['basedir'] : '';
29 $this->cache_dir = $basedir . '/master_addons/templates_kits/';
30 $this->purchased_dir = $basedir . '/master_addons/purchased_kits/';
31 $this->cache_expiry = apply_filters('jltma_template_kit_cache_expiry', 12 * HOUR_IN_SECONDS); // 12 hours default
32
33 // Initialize cache system
34 add_action('init', [$this, 'init'], 25);
35 }
36
37 public function init()
38 {
39 // Ensure cache directory exists
40 $this->ensure_cache_directory();
41
42 // Schedule cache updates
43 add_action('wp', [$this, 'schedule_cache_updates']);
44 add_action('jltma_template_kits_cache_update', [$this, 'update_template_kits_cache']);
45
46 // Admin hooks for cache management
47 add_action('admin_init', [$this, 'maybe_clear_cache']);
48 }
49
50 /**
51 * Ensure cache directory exists with proper structure for template kits
52 */
53 private function ensure_cache_directory()
54 {
55 // Check if uploads directory is writable
56 if (!$this->is_uploads_writable()) {
57 return false;
58 }
59
60 if (!file_exists($this->cache_dir)) {
61 if (!wp_mkdir_p($this->cache_dir)) {
62 return false;
63 }
64
65 // Create subdirectories for template kits with images folder
66 $subdirs = ['kits', 'manifests', 'thumbnails', 'previews', 'images', 'categories'];
67 foreach ($subdirs as $subdir) {
68 $subdir_path = $this->cache_dir . $subdir . '/';
69 if (!file_exists($subdir_path)) {
70 wp_mkdir_p($subdir_path);
71 }
72 }
73
74 // Create .htaccess for security
75 $htaccess_content = "Options -Indexes\n<Files \"*.json\">\nOrder allow,deny\nAllow from all\n</Files>";
76 file_put_contents($this->cache_dir . '.htaccess', $htaccess_content);
77
78 // Create index.php files
79 $index_content = "<?php\n// Silence is golden.\n";
80 file_put_contents($this->cache_dir . 'index.php', $index_content);
81
82 foreach ($subdirs as $subdir) {
83 file_put_contents($this->cache_dir . $subdir . '/index.php', $index_content);
84 }
85 }
86
87 return true;
88 }
89
90 /**
91 * Check if uploads directory is writable
92 */
93 private function is_uploads_writable()
94 {
95 $upload_dir = wp_upload_dir();
96 return file_exists($upload_dir['basedir']) && is_writable($upload_dir['basedir']);
97 }
98
99 /**
100 * Schedule cache update events for template kits via Background_Task_Manager
101 */
102 public function schedule_cache_updates()
103 {
104 Background_Task_Manager::get_instance()->schedule_recurring(
105 'jltma_template_kits_cache_update',
106 12 * HOUR_IN_SECONDS
107 );
108 }
109
110 /**
111 * Get cached kits for a specific category (public method)
112 */
113 public function get_category_kits($category, $force_refresh = false)
114 {
115 if ($category === 'all') {
116 return $this->get_cached_kits($force_refresh, 'all');
117 }
118
119 // Try to get category-specific cache
120 $category_data = $this->get_cached_category_kits($category, $force_refresh);
121
122 if ($category_data !== false) {
123 return $category_data;
124 }
125
126 // If category cache doesn't exist, get all and filter
127 $all_kits = $this->get_cached_kits($force_refresh, 'all');
128
129 if ($all_kits && is_array($all_kits)) {
130 // Filter kits by category
131 $filtered_kits = [];
132
133 foreach ($all_kits as $cat_key => $kits) {
134 if ($cat_key === $category && is_array($kits)) {
135 return $kits;
136 }
137
138 // Also check within kits
139 if (is_array($kits)) {
140 foreach ($kits as $kit) {
141 $kit_categories = $kit['categories'] ?? [];
142 if (is_string($kit_categories)) {
143 $kit_categories = [$kit_categories];
144 }
145 if (in_array($category, $kit_categories)) {
146 $filtered_kits[] = $kit;
147 }
148 }
149 }
150 }
151
152 // Save the filtered data as category cache for next time
153 if (!empty($filtered_kits)) {
154 $this->save_category_cache($category, $filtered_kits);
155 return $filtered_kits;
156 }
157 }
158
159 return [];
160 }
161
162 /**
163 * Get cached kit categories
164 */
165 public function get_cached_kit_categories($force_refresh = false)
166 {
167 // Try transient cache first if file cache is not available
168 if (!$this->is_file_cache_available()) {
169 return $this->get_transient_cached_kit_categories($force_refresh);
170 }
171
172 $cache_file = $this->cache_dir . 'kit-categories.json';
173 $cache_meta_file = $this->cache_dir . 'categories-meta.json';
174
175 // Check if cache exists and is valid
176 if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) {
177 $cached_data = $this->read_cache_file($cache_file);
178 if ($cached_data !== false) {
179 return $cached_data;
180 }
181 }
182
183 // Fetch fresh data from remote API
184 $fresh_data = $this->fetch_remote_kit_categories();
185
186 if ($fresh_data !== false) {
187 // Cache the data
188 $this->write_cache_file($cache_file, $fresh_data);
189 $this->write_cache_meta($cache_meta_file);
190 return $fresh_data;
191 }
192
193 // Fallback to expired cache if available
194 return $this->read_cache_file($cache_file);
195 }
196
197 /**
198 * Fetch kit categories from remote API
199 */
200 private function fetch_remote_kit_categories()
201 {
202 // Get config from the templates system
203 $config = null;
204 if (function_exists('MasterAddons\\Inc\\Admin\\Templates\\master_addons_templates')) {
205 $templates_instance = \MasterAddons\Inc\Admin\Templates\master_addons_templates();
206 if ($templates_instance && isset($templates_instance->config)) {
207 $config = $templates_instance->config->get('api');
208 }
209 }
210
211 $api_url = $config['base'] . $config['path'] . $config['endpoints']['categories'] . 'template_kits';
212
213 // Add pro_enabled parameter if pro is enabled
214 if ($this->is_pro_enabled) {
215 $api_url = add_query_arg('pro_enabled', 'true', $api_url);
216 }
217
218 $response = wp_remote_get($api_url, [
219 'timeout' => 10,
220 'sslverify' => false,
221 'headers' => [
222 'User-Agent' => 'Master Addons Template Kit Cache/' . JLTMA_VER
223 ]
224 ]);
225
226 if (is_wp_error($response)) {
227 return false;
228 }
229
230 $body = wp_remote_retrieve_body($response);
231 $data = json_decode($body, true);
232
233 if (json_last_error() !== JSON_ERROR_NONE || !isset($data['success']) || !$data['success']) {
234 return false;
235 }
236
237 return isset($data['categories']) ? $data['categories'] : [];
238 }
239
240 /**
241 * Get cached kit categories using transients (fallback method)
242 */
243 private function get_transient_cached_kit_categories($force_refresh = false)
244 {
245 $transient_key = 'jltma_kit_categories';
246 $meta_transient_key = 'jltma_kit_categories_meta';
247
248 // Check if cache exists and is valid
249 if (!$force_refresh) {
250 $cached_meta = get_transient($meta_transient_key);
251 if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) {
252 $cached_data = get_transient($transient_key);
253 if ($cached_data !== false) {
254 return $cached_data;
255 }
256 }
257 }
258
259 // Fetch fresh data from remote API
260 $fresh_data = $this->fetch_remote_kit_categories();
261
262 if ($fresh_data !== false) {
263 // Cache the data using transients
264 set_transient($transient_key, $fresh_data, $this->cache_expiry);
265 set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry);
266 return $fresh_data;
267 }
268
269 // Return cached data even if expired
270 return get_transient($transient_key);
271 }
272
273 /**
274 * Get cached template kits
275 */
276 public function get_cached_kits($force_refresh = false, $category = 'all')
277 {
278 // Try transient cache first if file cache is not available
279 if (!$this->is_file_cache_available()) {
280 return $this->get_transient_cached_kits($force_refresh);
281 }
282
283 // For specific categories, try to load category-specific cache first
284 if ($category !== 'all') {
285 $category_data = $this->get_cached_category_kits($category, $force_refresh);
286 if ($category_data !== false) {
287 return $category_data;
288 }
289 }
290
291 $cache_file = $this->cache_dir . 'template-kits.json';
292 $cache_meta_file = $this->cache_dir . 'meta.json';
293
294 // Check if cache exists and is valid
295 if (!$force_refresh && $this->is_cache_valid($cache_meta_file)) {
296 $cached_data = $this->read_cache_file($cache_file);
297 if ($cached_data !== false) {
298 if( $category !== 'all') {
299 // Filter by category if needed
300 $filtered_data = [];
301 foreach ($cached_data as $kit_category => $kits) {
302 if ($kit_category === $category) {
303 $filtered_data = $kits;
304 break;
305 }
306 }
307 $cached_data = $filtered_data;
308 }
309 // Update thumbnail URLs to use local cache
310 $this->process_kit_thumbnails($cached_data);
311 return $cached_data;
312 }
313 }
314
315 // Fetch fresh data from remote API
316 $fresh_data = $this->fetch_remote_kits();
317
318 if ($fresh_data !== false) {
319 // Process and cache the data
320 $this->process_and_cache_kits($fresh_data);
321
322 // After processing, load the cached data
323 if ($category !== 'all') {
324 return $this->get_cached_category_kits($category, false);
325 } else {
326 return $this->read_cache_file($this->cache_dir . 'template-kits.json');
327 }
328 }
329
330 // Fallback to expired cache if available
331 return $this->read_cache_file($cache_file);
332 }
333
334 /**
335 * Get cached kits for a specific category
336 */
337 private function get_cached_category_kits($category, $force_refresh = false)
338 {
339 $category_file = $this->cache_dir . 'categories/' . sanitize_file_name($category) . '.json';
340 $category_meta_file = $this->cache_dir . 'categories/' . sanitize_file_name($category) . '_meta.json';
341
342 // Ensure categories directory exists
343 if (!file_exists($this->cache_dir . 'categories/')) {
344 wp_mkdir_p($this->cache_dir . 'categories/');
345 }
346
347 // Check if category cache exists and is valid
348 if (!$force_refresh && $this->is_cache_valid($category_meta_file)) {
349 $cached_data = $this->read_cache_file($category_file);
350 if ($cached_data !== false) {
351 // Process thumbnails for cached data
352 $this->process_kit_thumbnails($cached_data);
353 return $cached_data;
354 }
355 }
356
357 return false;
358 }
359
360 /**
361 * Save category-specific cache
362 */
363 private function save_category_cache($category, $kits)
364 {
365 $category_file = $this->cache_dir . 'categories/' . sanitize_file_name($category) . '.json';
366 $category_meta_file = $this->cache_dir . 'categories/' . sanitize_file_name($category) . '_meta.json';
367
368 // Ensure categories directory exists
369 if (!file_exists($this->cache_dir . 'categories/')) {
370 wp_mkdir_p($this->cache_dir . 'categories/');
371 }
372
373 // Write category data
374 $this->write_cache_file($category_file, $kits);
375 $this->write_cache_meta($category_meta_file);
376 }
377
378 /**
379 * Process and cache kits data
380 */
381 private function process_and_cache_kits($fresh_data)
382 {
383 if (!is_array($fresh_data)) {
384 return;
385 }
386
387 // Ensure kits directory exists
388 $kits_dir = $this->cache_dir . 'kits/';
389 if (!file_exists($kits_dir)) {
390 wp_mkdir_p($kits_dir);
391 }
392
393 // The API returns kits already organized by category like:
394 // { "business": [...], "design": [...], "agency": [...] }
395 // So we just need to process the thumbnails and save the data
396
397 $categories_data = [];
398 $all_kits = [];
399
400 // Process each category
401 foreach ($fresh_data as $category => $kits) {
402 if (!is_array($kits)) {
403 continue;
404 }
405
406 $categories_data[$category] = [];
407
408 // Process each kit in the category
409 foreach ($kits as &$kit) {
410 if (!is_array($kit) || !isset($kit['kit_id'])) {
411 continue;
412 }
413
414 $kit_name = $kit['kit_name'] ?? $kit['name'] ?? '';
415
416 // Download and update thumbnail URL
417 if (isset($kit['thumbnail']) && !empty($kit['thumbnail'])) {
418 $local_url = $this->cache_image($kit['thumbnail'], "kit-{$kit_name}-thumb", 'thumbnails');
419 if ($local_url) {
420 $kit['thumbnail'] = $local_url;
421 }
422 }
423
424 // Download and update preview URL
425 if (isset($kit['preview']) && !empty($kit['preview'])) {
426 $local_url = $this->cache_image($kit['preview'], "kit-{$kit_name}-preview", 'previews');
427 if ($local_url) {
428 $kit['preview'] = $local_url;
429 }
430 }
431
432 // Process individual template thumbnails if available
433 if (isset($kit['templates']) && is_array($kit['templates'])) {
434 foreach ($kit['templates'] as &$template) {
435 if (isset($template['thumbnail']) && !empty($template['thumbnail'])) {
436 $template_name = $template['name'] ?? 'template';
437 $local_url = $this->cache_image(
438 $template['thumbnail'],
439 "kit-{$kit_name}-{$template_name}",
440 'thumbnails'
441 );
442 if ($local_url) {
443 $template['thumbnail'] = $local_url;
444 }
445 }
446 }
447 }
448
449 // Ensure categories field is properly set
450 if (!isset($kit['categories'])) {
451 $kit['categories'] = $category;
452 } elseif (is_string($kit['categories'])) {
453 $kit['categories'] = [$kit['categories']];
454 } elseif (!is_array($kit['categories'])) {
455 $kit['categories'] = [$category];
456 }
457
458 // Add to category data
459 $categories_data[$category][] = $kit;
460
461 // Also keep track of all kits
462 $all_kits[] = $kit;
463
464 // Automatically download and cache the kit if not already cached
465 if (isset($kit['kit_id'])) {
466 $this->maybe_download_kit($kit['kit_id']);
467 }
468 }
469 }
470
471 // Save main cache file organized by categories
472 $this->write_cache_file($this->cache_dir . 'template-kits.json', $categories_data);
473 $this->write_cache_meta($this->cache_dir . 'meta.json');
474
475 // Save individual category cache files
476 foreach ($categories_data as $category => $kits) {
477 if (!empty($kits)) {
478 $this->save_category_cache($category, $kits);
479 }
480 }
481 }
482
483 /**
484 * Process kit thumbnails (helper method)
485 */
486 private function process_kit_thumbnails(&$kits)
487 {
488 if (!is_array($kits)) {
489 return;
490 }
491
492 foreach ($kits as &$kit) {
493 if (isset($kit['thumbnail'])) {
494 $kit_name = $kit['name'] ?? $kit['kit_name'] ?? '';
495 $cached_thumbnail = $this->get_kit_thumbnail_url($kit_name, 'home', $kit['thumbnail']);
496 if ($cached_thumbnail) {
497 $kit['thumbnail'] = $cached_thumbnail;
498 }
499 }
500 }
501 }
502
503 /**
504 * Fetch template kits from remote API
505 */
506 private function fetch_remote_kits()
507 {
508 // Get config from the templates system
509 $config = null;
510 if (function_exists('MasterAddons\\Inc\\Admin\\Templates\\master_addons_templates')) {
511 $templates_instance = \MasterAddons\Inc\Admin\Templates\master_addons_templates();
512 if ($templates_instance && isset($templates_instance->config)) {
513 $config = $templates_instance->config->get('api');
514 }
515 }
516
517
518 // Build API URL for template kits (use templates endpoint with template-kits path)
519 $api_url = $config['base'] . $config['path'] . '/template-kits/';
520
521 // Add pro_enabled parameter if pro is enabled
522 if ($this->is_pro_enabled) {
523 $api_url = add_query_arg('pro_enabled', 'true', $api_url);
524 }
525
526 $response = wp_remote_get($api_url, [
527 'timeout' => 10,
528 'sslverify' => false,
529 'headers' => [
530 'User-Agent' => 'Master Addons Template Kit Cache/' . JLTMA_VER
531 ]
532 ]);
533
534 if (is_wp_error($response)) {
535 return false;
536 }
537
538 $body = wp_remote_retrieve_body($response);
539 $data = json_decode($body, true);
540
541 if (json_last_error() !== JSON_ERROR_NONE || !isset($data['success']) || !$data['success']) {
542 return false;
543 }
544
545 return isset($data['kits']) ? $data['kits'] : [];
546 }
547
548 /**
549 * Cache template kit images locally
550 */
551 private function cache_kit_images($kits)
552 {
553 if (!is_array($kits)) {
554 return;
555 }
556
557 foreach ($kits as $kit) {
558 $kit_name = $kit['name'] ?? '';
559
560 if (isset($kit['thumbnail']) && !empty($kit['thumbnail'])) {
561 $this->cache_image($kit['thumbnail'], "kit-{$kit_name}-thumb", 'thumbnails');
562 }
563
564 if (isset($kit['preview']) && !empty($kit['preview'])) {
565 $this->cache_image($kit['preview'], "kit-{$kit_name}-preview", 'previews');
566 }
567
568 // Cache individual template thumbnails if available
569 if (isset($kit['templates']) && is_array($kit['templates'])) {
570 foreach ($kit['templates'] as $template) {
571 if (isset($template['thumbnail']) && !empty($template['thumbnail'])) {
572 $template_name = $template['name'] ?? 'template';
573 $this->cache_image($template['thumbnail'], "kit-{$kit_name}-{$template_name}", 'thumbnails');
574 }
575 }
576 }
577 }
578 }
579
580 /**
581 * Cache individual image and return URL
582 * @param string $image_url The URL of the image to cache
583 * @param string $filename The filename to save as (without extension)
584 * @param string $folder The subfolder to save in (thumbnails, previews, images)
585 * @return string|false Local URL or false on failure
586 */
587 private function cache_image($image_url, $filename, $folder = 'images')
588 {
589 if (empty($image_url)) {
590 return false;
591 }
592
593 // Parse the URL to get the extension
594 $parsed_url = parse_url($image_url);
595 $path = $parsed_url['path'] ?? '';
596 $extension = pathinfo($path, PATHINFO_EXTENSION);
597
598 // Handle cases where extension might be in query string
599 if (empty($extension) || strlen($extension) > 4) {
600 // Try to determine from content type
601 $extension = 'jpg'; // Default fallback
602 }
603
604 // Sanitize filename
605 $filename = sanitize_file_name($filename);
606
607 $local_file = $this->cache_dir . "{$folder}/{$filename}.{$extension}";
608
609 // Skip downloading if already cached and recent, but still return URL
610 if (file_exists($local_file) && (time() - filemtime($local_file)) < DAY_IN_SECONDS) {
611 // Convert to URL and return
612 $upload_dir = wp_upload_dir();
613 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
614 return $upload_dir['baseurl'] . $relative_path;
615 }
616
617 // Ensure the folder exists
618 $folder_path = $this->cache_dir . $folder;
619 if (!file_exists($folder_path)) {
620 wp_mkdir_p($folder_path);
621 }
622
623 $response = wp_remote_get($image_url, [
624 'timeout' => 30,
625 'sslverify' => false,
626 'headers' => [
627 'User-Agent' => 'Master Addons Image Cache/' . JLTMA_VER
628 ]
629 ]);
630
631 if (is_wp_error($response)) {
632 return false;
633 }
634
635 $image_data = wp_remote_retrieve_body($response);
636
637 if (file_put_contents($local_file, $image_data)) {
638 // Convert to URL and return
639 $upload_dir = wp_upload_dir();
640 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
641 return $upload_dir['baseurl'] . $relative_path;
642 }
643
644 return false;
645 }
646
647 /**
648 * Update template kits cache (scheduled event)
649 */
650 public function update_template_kits_cache()
651 {
652 // Ensure kits directory exists
653 $kits_dir = $this->cache_dir . 'kits/';
654 if (!file_exists($kits_dir)) {
655 wp_mkdir_p($kits_dir);
656 }
657
658 // Update template kits with retry logic
659 try {
660 Background_Task_Manager::get_instance()->execute_with_retry(function () {
661 $this->get_cached_kits(true);
662 }, 3, 'template_kits_cache_sync');
663 } catch (\Exception $e) {
664 // Failure logged by execute_with_retry
665 }
666
667 // Clean up old cache files
668 $this->cleanup_old_cache();
669
670 // Update last cache time
671 set_transient('jltma_template_kits_last_cache_update', time(), DAY_IN_SECONDS);
672 }
673
674 /**
675 * Get template kit thumbnail from cache or return original URL
676 */
677 public function get_kit_thumbnail_url($kit_name, $template_name = 'home', $original_url = null)
678 {
679 // Normalize names for filename
680 $kit_slug = sanitize_title($kit_name);
681 $template_slug = sanitize_title($template_name);
682
683 // Check cache directory first
684 $cache_image_dir = $this->cache_dir . 'thumbnails/';
685 $cached_file_patterns = [
686 "kit-{$kit_slug}-{$template_slug}.jpg",
687 "kit-{$kit_slug}-{$template_slug}.png",
688 "kit-{$kit_slug}-thumb.jpg",
689 "kit-{$kit_slug}-thumb.png"
690 ];
691
692 foreach ($cached_file_patterns as $pattern) {
693 $cached_file = $cache_image_dir . $pattern;
694 if (file_exists($cached_file)) {
695 $upload_dir = wp_upload_dir();
696 $relative_path = str_replace($upload_dir['basedir'], '', $cached_file);
697 return $upload_dir['baseurl'] . $relative_path;
698 }
699 }
700
701 // Return original URL if provided
702 return $original_url;
703 }
704
705 /**
706 * Check if cache is valid
707 */
708 private function is_cache_valid($meta_file)
709 {
710 if (!file_exists($meta_file)) {
711 return false;
712 }
713
714 $meta = json_decode(file_get_contents($meta_file), true);
715 if (!$meta || !isset($meta['timestamp'])) {
716 return false;
717 }
718
719 $extended_cache_expiry = 24 * HOUR_IN_SECONDS;
720 return (time() - $meta['timestamp']) < $extended_cache_expiry;
721 }
722
723 /**
724 * Read cache file
725 */
726 private function read_cache_file($file_path)
727 {
728 if (!file_exists($file_path)) {
729 return false;
730 }
731
732 $content = file_get_contents($file_path);
733 if ($content === false) {
734 return false;
735 }
736
737 $data = json_decode($content, true);
738 return json_last_error() === JSON_ERROR_NONE ? $data : false;
739 }
740
741 /**
742 * Write cache file
743 */
744 private function write_cache_file($file_path, $data)
745 {
746 $dir = dirname($file_path);
747 if (!file_exists($dir)) {
748 wp_mkdir_p($dir);
749 }
750
751 $json = wp_json_encode($data, JSON_PRETTY_PRINT);
752 return file_put_contents($file_path, $json) !== false;
753 }
754
755 /**
756 * Write cache metadata
757 */
758 private function write_cache_meta($meta_file)
759 {
760 $meta = [
761 'timestamp' => time(),
762 'version' => JLTMA_VER,
763 'expiry' => $this->cache_expiry
764 ];
765
766 return file_put_contents($meta_file, wp_json_encode($meta)) !== false;
767 }
768
769 /**
770 * Clear all template kit cache (preserves purchased templates)
771 */
772 public function clear_cache()
773 {
774 $cleared = false;
775
776 // Clear file cache if available, but preserve purchased templates
777 if (file_exists($this->cache_dir)) {
778 // Note: Purchased templates are stored in a separate directory ($this->purchased_dir)
779 // So they won't be affected by clearing the regular cache
780 $this->delete_directory_contents($this->cache_dir);
781 $this->ensure_cache_directory();
782 $cleared = true;
783 }
784
785 // Clear transient cache
786 $this->clear_transient_cache();
787
788 delete_transient('jltma_template_kits_last_cache_update');
789
790 return true;
791 }
792
793 /**
794 * Refresh cache by clearing and fetching fresh data from API
795 */
796 public function refresh_cache()
797 {
798 // Clear all existing cache
799 $this->clear_cache();
800
801 // Force fetch fresh data from API
802 $kits = $this->get_cached_kits(true, 'all');
803
804 // Process all existing kit manifests to fix URLs
805 $this->reprocess_all_kit_manifests();
806
807 // Update last cache refresh time
808 set_transient('jltma_template_kits_last_cache_update', time(), DAY_IN_SECONDS);
809
810 // Count total kits
811 $total_kits = 0;
812 if (is_array($kits)) {
813 foreach ($kits as $category => $category_kits) {
814 if (is_array($category_kits)) {
815 $total_kits += count($category_kits);
816 }
817 }
818 }
819
820 return [
821 'kits' => $total_kits,
822 'categories' => is_array($kits) ? array_keys($kits) : []
823 ];
824 }
825
826 /**
827 * Reprocess all existing kit manifests to fix URLs
828 */
829 private function reprocess_all_kit_manifests() {
830 // Process regular cached kits
831 $kits_dir = $this->cache_dir . 'kits/';
832 if (file_exists($kits_dir)) {
833 // Get all kit directories
834 $kit_dirs = glob($kits_dir . '*', GLOB_ONLYDIR);
835
836 foreach ($kit_dirs as $kit_dir) {
837 // Process manifest
838 $this->process_extracted_kit_manifest($kit_dir);
839
840 // Process template JSON files
841 $this->process_template_json_files($kit_dir);
842
843 // Process nav_menu.json
844 $this->process_nav_menu_json($kit_dir);
845 }
846 }
847
848 // Process purchased kits
849 $this->reprocess_all_purchased_kits();
850 }
851
852 /**
853 * Reprocess all purchased kits to fix URLs
854 */
855 public function reprocess_all_purchased_kits() {
856 $purchased_kits_dir = $this->purchased_dir . 'kits/';
857
858 if (!file_exists($purchased_kits_dir)) {
859 return;
860 }
861
862 // Process main kit JSON files (e.g., 9960.json, 9966.json)
863 $main_json_files = glob($purchased_kits_dir . '*.json');
864 foreach ($main_json_files as $json_file) {
865 $this->process_purchased_kit_main_json($json_file);
866 }
867
868 // Get all purchased kit directories
869 $kit_dirs = glob($purchased_kits_dir . 'kit_*', GLOB_ONLYDIR);
870
871 foreach ($kit_dirs as $kit_dir) {
872 // Process manifest
873 $this->process_extracted_kit_manifest($kit_dir);
874
875 // Process template JSON files
876 $this->process_template_json_files($kit_dir);
877
878 // Process nav_menu.json
879 $this->process_nav_menu_json($kit_dir);
880 }
881 }
882
883 /**
884 * Process main purchased kit JSON file to replace remote URLs
885 * @param string $json_file Path to the JSON file
886 */
887 private function process_purchased_kit_main_json($json_file) {
888 if (!file_exists($json_file)) {
889 return;
890 }
891
892 $content = file_get_contents($json_file);
893 $data = json_decode($content, true);
894
895 if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
896 return;
897 }
898
899 $kit_id = $data['kit_id'] ?? basename($json_file, '.json');
900 $updated = false;
901
902 // Process main thumbnail
903 if (isset($data['thumbnail']) && !empty($data['thumbnail'])) {
904 $local_url = $this->process_purchased_kit_image($data['thumbnail'], $kit_id, 'main-thumb');
905 if ($local_url) {
906 $data['thumbnail'] = $local_url;
907 $updated = true;
908 }
909 }
910
911 // Process preview_url if it's an image
912 if (isset($data['preview_url']) && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $data['preview_url'])) {
913 $local_url = $this->process_purchased_kit_image($data['preview_url'], $kit_id, 'preview');
914 if ($local_url) {
915 $data['preview_url'] = $local_url;
916 $updated = true;
917 }
918 }
919
920 // Process templates array
921 if (isset($data['templates']) && is_array($data['templates'])) {
922 foreach ($data['templates'] as &$template) {
923 // Process screenshot
924 if (isset($template['screenshot']) && !empty($template['screenshot'])) {
925 // If it's already a relative path, leave it
926 if (strpos($template['screenshot'], 'screenshots/') === 0) {
927 continue;
928 }
929
930 $template_id = $template['template_id'] ?? uniqid();
931 $local_url = $this->process_purchased_kit_image(
932 $template['screenshot'],
933 $kit_id,
934 "template-{$template_id}-screenshot"
935 );
936 if ($local_url) {
937 $template['screenshot'] = $local_url;
938 $updated = true;
939 }
940 }
941
942 // Process thumbnail if exists
943 if (isset($template['thumbnail']) && !empty($template['thumbnail'])) {
944 $template_id = $template['template_id'] ?? uniqid();
945 $local_url = $this->process_purchased_kit_image(
946 $template['thumbnail'],
947 $kit_id,
948 "template-{$template_id}-thumb"
949 );
950 if ($local_url) {
951 $template['thumbnail'] = $local_url;
952 $updated = true;
953 }
954 }
955 }
956 }
957
958 // Process manifest data if exists
959 if (isset($data['manifest']) && is_array($data['manifest'])) {
960 // Process manifest thumbnail
961 if (isset($data['manifest']['thumbnail'])) {
962 $local_url = $this->process_purchased_kit_image(
963 $data['manifest']['thumbnail'],
964 $kit_id,
965 'manifest-thumb'
966 );
967 if ($local_url) {
968 $data['manifest']['thumbnail'] = $local_url;
969 $updated = true;
970 }
971 }
972
973 // Process manifest templates
974 if (isset($data['manifest']['templates']) && is_array($data['manifest']['templates'])) {
975 foreach ($data['manifest']['templates'] as &$template) {
976 if (isset($template['screenshot']) && !empty($template['screenshot'])) {
977 // Skip if already relative
978 if (strpos($template['screenshot'], 'screenshots/') === 0) {
979 continue;
980 }
981
982 $template_id = $template['template_id'] ?? uniqid();
983 $local_url = $this->process_purchased_kit_image(
984 $template['screenshot'],
985 $kit_id,
986 "manifest-template-{$template_id}"
987 );
988 if ($local_url) {
989 $template['screenshot'] = $local_url;
990 $updated = true;
991 }
992 }
993 }
994 }
995 }
996
997 // Save the updated JSON if changes were made
998 if ($updated) {
999 $json = wp_json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
1000 file_put_contents($json_file, $json);
1001 }
1002 }
1003
1004 /**
1005 * Process and cache a purchased kit image
1006 * @param string $image_url The image URL
1007 * @param string $kit_id The kit ID
1008 * @param string $filename_prefix Filename prefix
1009 * @return string|false Local URL or false
1010 */
1011 private function process_purchased_kit_image($image_url, $kit_id, $filename_prefix) {
1012 if (empty($image_url)) {
1013 return false;
1014 }
1015
1016 // Check if it's already a local URL
1017 $upload_dir = wp_upload_dir();
1018 if (strpos($image_url, $upload_dir['baseurl']) === 0) {
1019 // Clean up any double slashes
1020 return preg_replace('#(?<!:)//+#', '/', $image_url);
1021 }
1022
1023 // Skip if not a valid URL
1024 if (!filter_var($image_url, FILTER_VALIDATE_URL)) {
1025 return false;
1026 }
1027
1028 // Generate filename
1029 $url_hash = md5($image_url);
1030 $filename = "kit-{$kit_id}-{$filename_prefix}-{$url_hash}";
1031
1032 // Set cache directory for purchased kits
1033 $cache_base = $this->purchased_dir;
1034 $folder = 'images';
1035
1036 // Try to detect extension from URL
1037 $parsed_url = parse_url($image_url);
1038 $path = $parsed_url['path'] ?? '';
1039 $extension = pathinfo($path, PATHINFO_EXTENSION);
1040
1041 if (empty($extension) || strlen($extension) > 4) {
1042 $extension = 'jpg';
1043 }
1044
1045 $local_file = $cache_base . "{$folder}/{$filename}.{$extension}";
1046
1047 // Check if already cached
1048 if (file_exists($local_file)) {
1049 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
1050 return $upload_dir['baseurl'] . $relative_path;
1051 }
1052
1053 // Ensure folder exists
1054 $folder_path = $cache_base . $folder;
1055 if (!file_exists($folder_path)) {
1056 wp_mkdir_p($folder_path);
1057 }
1058
1059 // Download the image
1060 $response = wp_remote_get($image_url, [
1061 'timeout' => 30,
1062 'sslverify' => false,
1063 'headers' => [
1064 'User-Agent' => 'Master Addons Image Cache/' . JLTMA_VER
1065 ]
1066 ]);
1067
1068 if (is_wp_error($response)) {
1069 return false;
1070 }
1071
1072 $image_data = wp_remote_retrieve_body($response);
1073
1074 if (file_put_contents($local_file, $image_data)) {
1075 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
1076 return $upload_dir['baseurl'] . $relative_path;
1077 }
1078
1079 return false;
1080 }
1081
1082 /**
1083 * Process kit data to convert remote URLs to local URLs
1084 * @param array $kit_data The kit data to process
1085 * @param string $kit_id The kit ID
1086 * @return array The processed kit data with local URLs
1087 */
1088 private function process_kit_data_urls($kit_data, $kit_id) {
1089 $processed_data = $kit_data;
1090
1091 // Process main thumbnail
1092 if (isset($processed_data['thumbnail']) && !empty($processed_data['thumbnail'])) {
1093 $local_url = $this->process_purchased_kit_image($processed_data['thumbnail'], $kit_id, 'main-thumb');
1094 if ($local_url) {
1095 $processed_data['thumbnail'] = $local_url;
1096 }
1097 }
1098
1099 // Process preview_url if it's an image
1100 if (isset($processed_data['preview_url']) && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $processed_data['preview_url'])) {
1101 $local_url = $this->process_purchased_kit_image($processed_data['preview_url'], $kit_id, 'preview');
1102 if ($local_url) {
1103 $processed_data['preview_url'] = $local_url;
1104 }
1105 }
1106
1107 // Process templates array
1108 if (isset($processed_data['templates']) && is_array($processed_data['templates'])) {
1109 foreach ($processed_data['templates'] as &$template) {
1110 // Process screenshot
1111 if (isset($template['screenshot']) && !empty($template['screenshot'])) {
1112 // Skip if it's already a relative path
1113 if (strpos($template['screenshot'], 'screenshots/') !== 0) {
1114 $template_id = $template['template_id'] ?? $template['id'] ?? uniqid();
1115 $local_url = $this->process_purchased_kit_image(
1116 $template['screenshot'],
1117 $kit_id,
1118 "template-{$template_id}-screenshot"
1119 );
1120 if ($local_url) {
1121 $template['screenshot'] = $local_url;
1122 }
1123 }
1124 }
1125
1126 // Process thumbnail if exists
1127 if (isset($template['thumbnail']) && !empty($template['thumbnail'])) {
1128 $template_id = $template['template_id'] ?? $template['id'] ?? uniqid();
1129 $local_url = $this->process_purchased_kit_image(
1130 $template['thumbnail'],
1131 $kit_id,
1132 "template-{$template_id}-thumb"
1133 );
1134 if ($local_url) {
1135 $template['thumbnail'] = $local_url;
1136 }
1137 }
1138
1139 // Process preview_url if it's an image
1140 if (isset($template['preview_url']) && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $template['preview_url'])) {
1141 $template_id = $template['template_id'] ?? $template['id'] ?? uniqid();
1142 $local_url = $this->process_purchased_kit_image(
1143 $template['preview_url'],
1144 $kit_id,
1145 "template-{$template_id}-preview"
1146 );
1147 if ($local_url) {
1148 $template['preview_url'] = $local_url;
1149 }
1150 }
1151 }
1152 }
1153
1154 // Process manifest data if exists
1155 if (isset($processed_data['manifest']) && is_array($processed_data['manifest'])) {
1156 // Process manifest thumbnail
1157 if (isset($processed_data['manifest']['thumbnail']) && !empty($processed_data['manifest']['thumbnail'])) {
1158 $local_url = $this->process_purchased_kit_image(
1159 $processed_data['manifest']['thumbnail'],
1160 $kit_id,
1161 'manifest-thumb'
1162 );
1163 if ($local_url) {
1164 $processed_data['manifest']['thumbnail'] = $local_url;
1165 }
1166 }
1167
1168 // Process manifest thumbnail_url
1169 if (isset($processed_data['manifest']['thumbnail_url']) && !empty($processed_data['manifest']['thumbnail_url'])) {
1170 $local_url = $this->process_purchased_kit_image(
1171 $processed_data['manifest']['thumbnail_url'],
1172 $kit_id,
1173 'manifest-thumb-url'
1174 );
1175 if ($local_url) {
1176 $processed_data['manifest']['thumbnail_url'] = $local_url;
1177 }
1178 }
1179
1180 // Process manifest preview_url if it's an image
1181 if (isset($processed_data['manifest']['preview_url']) && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $processed_data['manifest']['preview_url'])) {
1182 $local_url = $this->process_purchased_kit_image(
1183 $processed_data['manifest']['preview_url'],
1184 $kit_id,
1185 'manifest-preview'
1186 );
1187 if ($local_url) {
1188 $processed_data['manifest']['preview_url'] = $local_url;
1189 }
1190 }
1191
1192 // Process manifest templates
1193 if (isset($processed_data['manifest']['templates']) && is_array($processed_data['manifest']['templates'])) {
1194 foreach ($processed_data['manifest']['templates'] as &$template) {
1195 if (isset($template['screenshot']) && !empty($template['screenshot'])) {
1196 // Skip if already relative
1197 if (strpos($template['screenshot'], 'screenshots/') !== 0) {
1198 $template_id = $template['template_id'] ?? $template['id'] ?? uniqid();
1199 $local_url = $this->process_purchased_kit_image(
1200 $template['screenshot'],
1201 $kit_id,
1202 "manifest-template-{$template_id}"
1203 );
1204 if ($local_url) {
1205 $template['screenshot'] = $local_url;
1206 }
1207 }
1208 }
1209
1210 if (isset($template['thumbnail']) && !empty($template['thumbnail'])) {
1211 $template_id = $template['template_id'] ?? $template['id'] ?? uniqid();
1212 $local_url = $this->process_purchased_kit_image(
1213 $template['thumbnail'],
1214 $kit_id,
1215 "manifest-template-{$template_id}-thumb"
1216 );
1217 if ($local_url) {
1218 $template['thumbnail'] = $local_url;
1219 }
1220 }
1221 }
1222 }
1223
1224 // Process manifest pages (alternative structure)
1225 if (isset($processed_data['manifest']['pages']) && is_array($processed_data['manifest']['pages'])) {
1226 foreach ($processed_data['manifest']['pages'] as &$page) {
1227 if (isset($page['screenshot']) && !empty($page['screenshot'])) {
1228 $page_id = $page['page_id'] ?? $page['id'] ?? uniqid();
1229 $local_url = $this->process_purchased_kit_image(
1230 $page['screenshot'],
1231 $kit_id,
1232 "manifest-page-{$page_id}"
1233 );
1234 if ($local_url) {
1235 $page['screenshot'] = $local_url;
1236 }
1237 }
1238
1239 if (isset($page['thumbnail']) && !empty($page['thumbnail'])) {
1240 $page_id = $page['page_id'] ?? $page['id'] ?? uniqid();
1241 $local_url = $this->process_purchased_kit_image(
1242 $page['thumbnail'],
1243 $kit_id,
1244 "manifest-page-{$page_id}-thumb"
1245 );
1246 if ($local_url) {
1247 $page['thumbnail'] = $local_url;
1248 }
1249 }
1250 }
1251 }
1252
1253 // Process images array in manifest
1254 if (isset($processed_data['manifest']['images']) && is_array($processed_data['manifest']['images'])) {
1255 foreach ($processed_data['manifest']['images'] as &$image) {
1256 // Process thumbnail_url
1257 if (isset($image['thumbnail_url']) && !empty($image['thumbnail_url'])) {
1258 $filename = $image['filename'] ?? uniqid();
1259 $local_url = $this->process_purchased_kit_image(
1260 $image['thumbnail_url'],
1261 $kit_id,
1262 "manifest-image-" . pathinfo($filename, PATHINFO_FILENAME)
1263 );
1264 if ($local_url) {
1265 $image['thumbnail_url'] = $local_url;
1266 }
1267 }
1268
1269 // Process image_urls if it contains URLs
1270 if (isset($image['image_urls']) && !empty($image['image_urls']) && filter_var($image['image_urls'], FILTER_VALIDATE_URL)) {
1271 $filename = $image['filename'] ?? uniqid();
1272 $local_url = $this->process_purchased_kit_image(
1273 $image['image_urls'],
1274 $kit_id,
1275 "manifest-image-url-" . pathinfo($filename, PATHINFO_FILENAME)
1276 );
1277 if ($local_url) {
1278 $image['image_urls'] = $local_url;
1279 }
1280 }
1281 }
1282 }
1283 }
1284
1285 // Process content if it contains Elementor data with images
1286 if (isset($processed_data['content']) && is_array($processed_data['content'])) {
1287 $processed_data['content'] = $this->process_elementor_data_for_images(
1288 $processed_data['content'],
1289 $kit_id,
1290 'content',
1291 null
1292 );
1293 }
1294
1295 return $processed_data;
1296 }
1297
1298 /**
1299 * Clean up old cache files
1300 */
1301 private function cleanup_old_cache()
1302 {
1303 $subdirs = ['kits', 'manifests', 'thumbnails', 'previews', 'images', 'categories'];
1304 $max_age = 30 * DAY_IN_SECONDS; // 30 days
1305
1306 foreach ($subdirs as $subdir) {
1307 $full_dir = $this->cache_dir . $subdir . '/';
1308 if (!file_exists($full_dir)) {
1309 // Create the directory if it doesn't exist
1310 wp_mkdir_p($full_dir);
1311 continue;
1312 }
1313
1314 // For kits directory, handle subdirectories
1315 if ($subdir === 'kits') {
1316 $kit_dirs = glob($full_dir . '*', GLOB_ONLYDIR);
1317 foreach ($kit_dirs as $kit_dir) {
1318 $meta_file = $kit_dir . '/meta.json';
1319 if (file_exists($meta_file) && (time() - filemtime($meta_file)) > $max_age) {
1320 $this->delete_directory_contents($kit_dir);
1321 @rmdir($kit_dir);
1322 }
1323 }
1324 } else {
1325 $files = glob($full_dir . '*');
1326 foreach ($files as $file) {
1327 if (is_file($file) && (time() - filemtime($file)) > $max_age) {
1328 unlink($file);
1329 }
1330 }
1331 }
1332 }
1333 }
1334
1335 /**
1336 * Delete directory contents recursively
1337 * @param string $dir Directory path
1338 * @param array $exclude_dirs Directories to exclude from deletion
1339 */
1340 private function delete_directory_contents($dir, $exclude_dirs = [])
1341 {
1342 if (!file_exists($dir)) {
1343 return;
1344 }
1345
1346 $files = glob($dir . '*', GLOB_MARK);
1347 foreach ($files as $file) {
1348 if (is_dir($file)) {
1349 // Check if this directory should be excluded
1350 $should_exclude = false;
1351 foreach ($exclude_dirs as $exclude_dir) {
1352 if (strpos($file, $exclude_dir) !== false) {
1353 $should_exclude = true;
1354 break;
1355 }
1356 }
1357
1358 if (!$should_exclude) {
1359 $this->delete_directory_contents($file, $exclude_dirs);
1360 @rmdir($file);
1361 }
1362 } else {
1363 // Don't delete files in excluded directories
1364 $in_excluded_dir = false;
1365 foreach ($exclude_dirs as $exclude_dir) {
1366 if (strpos($file, $exclude_dir) !== false) {
1367 $in_excluded_dir = true;
1368 break;
1369 }
1370 }
1371
1372 if (!$in_excluded_dir) {
1373 unlink($file);
1374 }
1375 }
1376 }
1377 }
1378
1379 /**
1380 * Handle cache clearing from admin
1381 */
1382 public function maybe_clear_cache()
1383 {
1384 if (isset($_GET['jltma_clear_template_kit_cache']) &&
1385 isset($_GET['_wpnonce']) &&
1386 wp_verify_nonce($_GET['_wpnonce'], 'jltma_clear_template_kit_cache') &&
1387 current_user_can('manage_options')) {
1388
1389 $this->clear_cache();
1390
1391 wp_redirect(add_query_arg([
1392 'jltma_template_kit_cache_cleared' => '1'
1393 ], remove_query_arg(['jltma_clear_template_kit_cache', '_wpnonce'])));
1394 exit;
1395 }
1396
1397 // Handle cache refresh from admin
1398 if (isset($_GET['jltma_refresh_template_kit_cache']) &&
1399 isset($_GET['_wpnonce']) &&
1400 wp_verify_nonce($_GET['_wpnonce'], 'jltma_refresh_template_kit_cache') &&
1401 current_user_can('manage_options')) {
1402
1403 $this->refresh_cache();
1404
1405 wp_redirect(add_query_arg([
1406 'jltma_template_kit_cache_refreshed' => '1'
1407 ], remove_query_arg(['jltma_refresh_template_kit_cache', '_wpnonce'])));
1408 exit;
1409 }
1410
1411 // Handle download all kits from admin
1412 if (isset($_GET['jltma_download_all_kits']) &&
1413 isset($_GET['_wpnonce']) &&
1414 wp_verify_nonce($_GET['_wpnonce'], 'jltma_download_all_kits') &&
1415 current_user_can('manage_options')) {
1416
1417 $result = $this->download_all_kits();
1418
1419 wp_redirect(add_query_arg([
1420 'jltma_kits_downloaded' => $result['downloaded'],
1421 'jltma_kits_failed' => $result['failed']
1422 ], remove_query_arg(['jltma_download_all_kits', '_wpnonce'])));
1423 exit;
1424 }
1425 }
1426
1427 /**
1428 * Get cache statistics
1429 */
1430 public function get_cache_stats()
1431 {
1432 $stats = [
1433 'cache_dir_exists' => file_exists($this->cache_dir),
1434 'cache_size' => $this->get_directory_size($this->cache_dir),
1435 'last_update' => get_transient('jltma_template_kits_last_cache_update'),
1436 'next_scheduled_update' => wp_next_scheduled('jltma_template_kits_cache_update'),
1437 'total_kits' => 0,
1438 'total_templates' => 0
1439 ];
1440
1441 // Count cached kits
1442 if ($this->is_file_cache_available()) {
1443 $kits_file = $this->cache_dir . 'template-kits.json';
1444 if (file_exists($kits_file)) {
1445 $kits_data = $this->read_cache_file($kits_file);
1446 if ($kits_data && is_array($kits_data)) {
1447 $stats['total_kits'] = count($kits_data);
1448 foreach ($kits_data as $kit) {
1449 if (isset($kit['templates']) && is_array($kit['templates'])) {
1450 $stats['total_templates'] += count($kit['templates']);
1451 }
1452 }
1453 }
1454 }
1455 } else {
1456 // Count from transients
1457 $cached_kits = get_transient('jltma_template_kits');
1458 if ($cached_kits && is_array($cached_kits)) {
1459 $stats['total_kits'] = count($cached_kits);
1460 foreach ($cached_kits as $kit) {
1461 if (isset($kit['templates']) && is_array($kit['templates'])) {
1462 $stats['total_templates'] += count($kit['templates']);
1463 }
1464 }
1465 }
1466 }
1467
1468 return $stats;
1469 }
1470
1471 /**
1472 * Get directory size in bytes
1473 */
1474 private function get_directory_size($dir)
1475 {
1476 $size = 0;
1477 if (file_exists($dir)) {
1478 foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS)) as $file) {
1479 if ($file->isFile()) {
1480 $size += $file->getSize();
1481 }
1482 }
1483 }
1484 return $size;
1485 }
1486
1487 /**
1488 * Check if file cache is available
1489 */
1490 private function is_file_cache_available()
1491 {
1492 return file_exists($this->cache_dir) && is_writable($this->cache_dir);
1493 }
1494
1495 /**
1496 * Get cached template kits using transients (fallback method)
1497 */
1498 private function get_transient_cached_kits($force_refresh = false)
1499 {
1500 $transient_key = 'jltma_template_kits';
1501 $meta_transient_key = 'jltma_template_kits_meta';
1502
1503 // Check if cache exists and is valid
1504 if (!$force_refresh) {
1505 $cached_meta = get_transient($meta_transient_key);
1506 if ($cached_meta && (time() - $cached_meta['timestamp']) < $this->cache_expiry) {
1507 $cached_data = get_transient($transient_key);
1508 if ($cached_data !== false) {
1509 // Update thumbnail URLs for transient cached kits
1510 foreach ($cached_data as &$kit) {
1511 if (isset($kit['thumbnail'])) {
1512 $cached_thumbnail = $this->get_kit_thumbnail_url($kit['name'], 'home', $kit['thumbnail']);
1513 if ($cached_thumbnail) {
1514 $kit['thumbnail'] = $cached_thumbnail;
1515 }
1516 }
1517 }
1518 return $cached_data;
1519 }
1520 }
1521 }
1522
1523 // Fetch fresh data from remote API
1524 $fresh_data = $this->fetch_remote_kits();
1525 $categories = ['all' => 'ALL Categories'];
1526
1527 if ($fresh_data !== false) {
1528 // Update thumbnail URLs for fresh transient kits
1529 foreach ($fresh_data as &$kit) {
1530 if (isset($kit['thumbnail'])) {
1531 $cached_thumbnail = $this->get_kit_thumbnail_url($kit['name'], 'home', $kit['thumbnail']);
1532 if ($cached_thumbnail) {
1533 $kit['thumbnail'] = $cached_thumbnail;
1534 }
1535 }
1536 }
1537
1538 // Cache the data using transients
1539 set_transient($transient_key, $fresh_data, $this->cache_expiry);
1540 set_transient($meta_transient_key, ['timestamp' => time()], $this->cache_expiry);
1541
1542 return $fresh_data;
1543 }
1544
1545 // Return cached data even if expired
1546 return get_transient($transient_key);
1547 }
1548
1549 /**
1550 * Clear transient cache (fallback method)
1551 */
1552 private function clear_transient_cache()
1553 {
1554 delete_transient('jltma_template_kits');
1555 delete_transient('jltma_template_kits_meta');
1556 delete_transient('jltma_kit_categories');
1557 delete_transient('jltma_kit_categories_meta');
1558
1559 // Clear kit template transients
1560 global $wpdb;
1561 $wpdb->query(
1562 "DELETE FROM {$wpdb->options}
1563 WHERE option_name LIKE '_transient_jltma_kit_templates_%'
1564 OR option_name LIKE '_transient_timeout_jltma_kit_templates_%'"
1565 );
1566 }
1567
1568 /**
1569 * Get cached kit content (templates inside a kit)
1570 */
1571 public function get_cached_kit_content($kit_id, $kit_category, $force_refresh = false) {
1572 // Check if file cache is available
1573 if (!$this->is_file_cache_available()) {
1574 return false;
1575 }
1576
1577 $cache_file = $this->cache_dir . $kit_category . '_' . $kit_id . '.json';
1578 $meta_file = $this->cache_dir . $kit_category . '_' . $kit_id . '_meta.json';
1579
1580 // Check if we need to refresh
1581 if (!$force_refresh && file_exists($cache_file) && file_exists($meta_file)) {
1582 if ($this->is_cache_valid($meta_file)) {
1583 $cached_data = $this->read_cache_file($cache_file);
1584 if ($cached_data !== false) {
1585 return $cached_data;
1586 }
1587 }
1588 }
1589
1590 return false;
1591 }
1592
1593 /**
1594 * Save kit content to cache
1595 */
1596 public function save_kit_content($kit_id, $kit_category, $templates) {
1597 // Check if file cache is available
1598 if (!$this->is_file_cache_available()) {
1599 return false;
1600 }
1601
1602 // Process templates to cache all images
1603 $templates = $this->process_and_cache_template_images($templates, $kit_id);
1604
1605 $cache_file = $this->cache_dir . $kit_category . '_' . $kit_id . '.json';
1606 $meta_file = $this->cache_dir . $kit_category . '_' . $kit_id . '_meta.json';
1607
1608 // Write cache file
1609 if ($this->write_cache_file($cache_file, $templates)) {
1610 // Write meta file
1611 $this->write_cache_meta($meta_file);
1612 return true;
1613 }
1614
1615 return false;
1616 }
1617
1618 /**
1619 * Process and cache all images in template content
1620 */
1621 private function process_and_cache_template_images($templates, $kit_id) {
1622 if (!is_array($templates)) {
1623 return $templates;
1624 }
1625
1626 foreach ($templates as &$template) {
1627 if (isset($template['content'])) {
1628 $template['content'] = $this->process_elementor_content_images($template['content'], $kit_id);
1629 }
1630 }
1631
1632 return $templates;
1633 }
1634
1635 /**
1636 * Process Elementor content to find and cache images
1637 */
1638 private function process_elementor_content_images($content, $kit_id) {
1639 if (is_string($content)) {
1640 $content = json_decode($content, true);
1641 }
1642
1643 if (!is_array($content)) {
1644 return $content;
1645 }
1646
1647 // Recursively process elements
1648 foreach ($content as &$element) {
1649 if (isset($element['settings'])) {
1650 $element['settings'] = $this->process_element_settings_images($element['settings'], $kit_id);
1651 }
1652
1653 if (isset($element['elements']) && is_array($element['elements'])) {
1654 $element['elements'] = $this->process_elementor_content_images($element['elements'], $kit_id);
1655 }
1656 }
1657
1658 return $content;
1659 }
1660
1661 /**
1662 * Process element settings to cache images
1663 */
1664 private function process_element_settings_images($settings, $kit_id) {
1665 if (!is_array($settings)) {
1666 return $settings;
1667 }
1668
1669 // Image-related settings to check
1670 $image_settings = [
1671 'image', 'background_image', 'hover_image', 'bg_image', 'icon_image',
1672 'gallery', 'images', 'slide_image', 'background_overlay_image',
1673 'testimonial_image', 'team_image', 'portfolio_image', 'logo_image',
1674 'before_image', 'after_image', 'author_image', 'product_image'
1675 ];
1676
1677 foreach ($settings as $key => &$value) {
1678 // Handle single image settings
1679 if (in_array($key, $image_settings) && is_array($value) && isset($value['url'])) {
1680 $cached_url = $this->cache_and_replace_image_url($value['url'], $kit_id, $key);
1681 if ($cached_url) {
1682 $value['url'] = $cached_url;
1683 }
1684 }
1685
1686 // Handle gallery settings (array of images)
1687 if ($key === 'gallery' && is_array($value)) {
1688 foreach ($value as &$gallery_item) {
1689 if (is_array($gallery_item) && isset($gallery_item['url'])) {
1690 $cached_url = $this->cache_and_replace_image_url($gallery_item['url'], $kit_id, 'gallery');
1691 if ($cached_url) {
1692 $gallery_item['url'] = $cached_url;
1693 }
1694 }
1695 }
1696 }
1697
1698 // Handle repeater fields that might contain images
1699 if (is_array($value) && !in_array($key, $image_settings)) {
1700 foreach ($value as &$repeater_item) {
1701 if (is_array($repeater_item)) {
1702 $repeater_item = $this->process_element_settings_images($repeater_item, $kit_id);
1703 }
1704 }
1705 }
1706 }
1707
1708 return $settings;
1709 }
1710
1711 /**
1712 * Cache an image and return the local URL
1713 */
1714 private function cache_and_replace_image_url($image_url, $kit_id, $context = 'content') {
1715 if (empty($image_url) || !filter_var($image_url, FILTER_VALIDATE_URL)) {
1716 return false;
1717 }
1718
1719 // Generate a unique filename based on the URL and context
1720 $url_hash = md5($image_url);
1721 $filename = "kit-{$kit_id}-{$context}-{$url_hash}";
1722
1723 // Cache the image
1724 $local_path = $this->cache_image($image_url, $filename, 'images');
1725
1726 if ($local_path) {
1727 // Convert local path to URL
1728 $upload_dir = wp_upload_dir();
1729 $relative_path = str_replace($upload_dir['basedir'], '', $local_path);
1730 return $upload_dir['baseurl'] . $relative_path;
1731 }
1732
1733 return false;
1734 }
1735
1736 /**
1737 * Get cached image URL
1738 */
1739 public function get_cached_image_url($original_url, $kit_id = '', $context = 'content') {
1740 if (empty($original_url)) {
1741 return $original_url;
1742 }
1743
1744 // Generate the expected filename
1745 $url_hash = md5($original_url);
1746 $filename = "kit-{$kit_id}-{$context}-{$url_hash}";
1747
1748 // Check for cached file with common extensions
1749 $extensions = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'];
1750 foreach ($extensions as $ext) {
1751 $local_file = $this->cache_dir . "images/{$filename}.{$ext}";
1752 if (file_exists($local_file)) {
1753 $upload_dir = wp_upload_dir();
1754 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
1755 return $upload_dir['baseurl'] . $relative_path;
1756 }
1757 }
1758
1759 return $original_url;
1760 }
1761
1762 /**
1763 * Download and cache kit manifest and ZIP file
1764 * @param string $kit_id The kit ID
1765 * @return array|false Array with kit data or false on failure
1766 */
1767 public function download_and_cache_kit($kit_id) {
1768 // Ensure kits directory exists
1769 $kits_dir = $this->cache_dir . 'kits/';
1770 if (!file_exists($kits_dir)) {
1771 wp_mkdir_p($kits_dir);
1772 }
1773
1774 // Create kit-specific directory
1775 $kit_dir = $kits_dir . sanitize_file_name($kit_id) . '/';
1776 if (!file_exists($kit_dir)) {
1777 wp_mkdir_p($kit_dir);
1778 }
1779
1780 // Check if kit is already cached and recent
1781 $manifest_file = $kit_dir . 'manifest.json';
1782 $meta_file = $kit_dir . 'meta.json';
1783
1784 // Check if cache is valid
1785 if (!$this->is_cache_valid($meta_file)) {
1786 // Get API config
1787 $config = null;
1788 if (function_exists('MasterAddons\\Inc\\Admin\\Templates\\master_addons_templates')) {
1789 $templates_instance = \MasterAddons\Inc\Admin\Templates\master_addons_templates();
1790 if ($templates_instance && isset($templates_instance->config)) {
1791 $config = $templates_instance->config->get('api');
1792 }
1793 }
1794
1795 if (!$config) {
1796 return false;
1797 }
1798
1799 // Build manifest URL
1800 $api_url = $config['base'] . $config['path'] . '/templates-kit/' . $kit_id . '/manifest.json';
1801
1802 // Add pro_enabled parameter if pro is enabled
1803 if ($this->is_pro_enabled) {
1804 $api_url = add_query_arg('pro_enabled', 'true', $api_url);
1805 }
1806
1807 // Fetch manifest from API
1808 $response = wp_remote_get($api_url, [
1809 'timeout' => 60,
1810 'sslverify' => false,
1811 'headers' => [
1812 'User-Agent' => 'Master Addons Kit Downloader/' . JLTMA_VER
1813 ]
1814 ]);
1815
1816 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1817 return false;
1818 }
1819
1820 $body = wp_remote_retrieve_body($response);
1821 $manifest_data = json_decode($body, true);
1822
1823 if (!$manifest_data || !isset($manifest_data['success']) || !$manifest_data['success']) {
1824 return false;
1825 }
1826
1827 // Check if we have a ZIP URL
1828 if (isset($manifest_data['data']) && is_string($manifest_data['data'])) {
1829 $zip_url = $manifest_data['data'];
1830
1831 // Download the ZIP file
1832 $zip_file = $kit_dir . 'kit.zip';
1833 $downloaded = $this->download_file($zip_url, $zip_file);
1834
1835 if ($downloaded) {
1836 // Extract the ZIP file
1837 $extracted = $this->extract_kit_zip($zip_file, $kit_dir);
1838
1839 if ($extracted) {
1840 // Delete the ZIP file after extraction
1841 @unlink($zip_file);
1842
1843 // Process manifest and template files
1844 $this->process_extracted_kit_manifest($kit_dir);
1845 $this->process_template_json_files($kit_dir);
1846 $this->process_nav_menu_json($kit_dir);
1847
1848 // Save meta information
1849 $this->write_cache_meta($meta_file);
1850
1851 // Return kit directory path
1852 return [
1853 'success' => true,
1854 'kit_dir' => $kit_dir,
1855 'manifest' => $this->get_kit_manifest($kit_id)
1856 ];
1857 }
1858 }
1859 }
1860 } else {
1861 // Cache is valid, return existing data
1862 return [
1863 'success' => true,
1864 'kit_dir' => $kit_dir,
1865 'manifest' => $this->get_kit_manifest($kit_id)
1866 ];
1867 }
1868
1869 return false;
1870 }
1871
1872 /**
1873 * Download a file from URL
1874 * @param string $url The URL to download from
1875 * @param string $destination The local file path to save to
1876 * @return bool Success or failure
1877 */
1878 private function download_file($url, $destination) {
1879 // Use WordPress download function
1880 require_once(ABSPATH . 'wp-admin/includes/file.php');
1881
1882 $tmp_file = download_url($url, 300); // 5 minutes timeout
1883
1884 if (is_wp_error($tmp_file)) {
1885 // Try alternative method
1886 $response = wp_remote_get($url, [
1887 'timeout' => 300,
1888 'sslverify' => false,
1889 'headers' => [
1890 'User-Agent' => 'Master Addons Kit Downloader/' . JLTMA_VER
1891 ]
1892 ]);
1893
1894 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1895 return false;
1896 }
1897
1898 $file_data = wp_remote_retrieve_body($response);
1899 return file_put_contents($destination, $file_data) !== false;
1900 }
1901
1902 // Move temp file to destination
1903 $moved = rename($tmp_file, $destination);
1904
1905 // Clean up temp file if move failed
1906 if (!$moved && file_exists($tmp_file)) {
1907 @unlink($tmp_file);
1908 }
1909
1910 return $moved;
1911 }
1912
1913 /**
1914 * Extract kit ZIP file
1915 * @param string $zip_file Path to ZIP file
1916 * @param string $destination Extraction destination
1917 * @return bool Success or failure
1918 */
1919 private function extract_kit_zip($zip_file, $destination) {
1920 if (!file_exists($zip_file)) {
1921 return false;
1922 }
1923
1924 // Use WordPress unzip function
1925 WP_Filesystem();
1926 $result = unzip_file($zip_file, $destination);
1927
1928 if (is_wp_error($result)) {
1929 // Try PHP ZipArchive as fallback
1930 if (class_exists('ZipArchive')) {
1931 $zip = new \ZipArchive();
1932 if ($zip->open($zip_file) === true) {
1933 $zip->extractTo($destination);
1934 $zip->close();
1935
1936 // Process manifest after extraction
1937 $this->process_extracted_kit_manifest($destination);
1938 return true;
1939 }
1940 }
1941 return false;
1942 }
1943
1944 // Process manifest after successful extraction
1945 $this->process_extracted_kit_manifest($destination);
1946
1947 // Process template JSON files
1948 $this->process_template_json_files($destination);
1949
1950 // Process nav_menu.json
1951 $this->process_nav_menu_json($destination);
1952
1953 return true;
1954 }
1955
1956 /**
1957 * Process extracted kit manifest to update all image URLs to local paths
1958 * @param string $kit_dir Path to the extracted kit directory
1959 * @return bool Success status
1960 */
1961 private function process_extracted_kit_manifest($kit_dir) {
1962 $manifest_file = $kit_dir . '/manifest.json';
1963
1964 if (!file_exists($manifest_file)) {
1965 return false;
1966 }
1967
1968 // Read the manifest
1969 $content = file_get_contents($manifest_file);
1970 $manifest = json_decode($content, true);
1971
1972 if (json_last_error() !== JSON_ERROR_NONE || !is_array($manifest)) {
1973 return false;
1974 }
1975
1976 // Extract kit_id from directory name or manifest
1977 $kit_id = basename($kit_dir);
1978 if (isset($manifest['kit_id'])) {
1979 $kit_id = $manifest['kit_id'];
1980 }
1981
1982 $updated = false;
1983
1984 // Process main kit thumbnail if exists
1985 if (isset($manifest['thumbnail'])) {
1986 $local_url = $this->process_and_cache_manifest_image($manifest['thumbnail'], $kit_dir, $kit_id, 'kit-main');
1987 if ($local_url) {
1988 $manifest['thumbnail'] = $local_url;
1989 $updated = true;
1990 }
1991 }
1992
1993 // Process thumbnail_url if exists
1994 if (isset($manifest['thumbnail_url'])) {
1995 $local_url = $this->process_and_cache_manifest_image($manifest['thumbnail_url'], $kit_dir, $kit_id, 'kit-thumb');
1996 if ($local_url) {
1997 $manifest['thumbnail_url'] = $local_url;
1998 $updated = true;
1999 }
2000 }
2001
2002 // Process preview_url if exists
2003 if (isset($manifest['preview_url'])) {
2004 // Preview URLs are usually live sites, so we might not want to cache them
2005 // But if it's an image URL, we can cache it
2006 if (preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $manifest['preview_url'])) {
2007 $local_url = $this->process_and_cache_manifest_image($manifest['preview_url'], $kit_dir, $kit_id, 'kit-preview');
2008 if ($local_url) {
2009 $manifest['preview_url'] = $local_url;
2010 $updated = true;
2011 }
2012 }
2013 }
2014
2015 // Process templates in manifest
2016 if (isset($manifest['templates']) && is_array($manifest['templates'])) {
2017 foreach ($manifest['templates'] as &$template) {
2018 $template_id = $template['template_id'] ?? $template['id'] ?? uniqid();
2019
2020 // Process screenshot
2021 if (isset($template['screenshot'])) {
2022 $local_url = $this->process_and_cache_manifest_image(
2023 $template['screenshot'],
2024 $kit_dir,
2025 $kit_id,
2026 "template-{$template_id}-screenshot"
2027 );
2028 if ($local_url) {
2029 $template['screenshot'] = $local_url;
2030 $updated = true;
2031 }
2032 }
2033
2034 // Process thumbnail
2035 if (isset($template['thumbnail'])) {
2036 $local_url = $this->process_and_cache_manifest_image(
2037 $template['thumbnail'],
2038 $kit_dir,
2039 $kit_id,
2040 "template-{$template_id}-thumb"
2041 );
2042 if ($local_url) {
2043 $template['thumbnail'] = $local_url;
2044 $updated = true;
2045 }
2046 }
2047
2048 // Process preview_url
2049 if (isset($template['preview_url']) && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $template['preview_url'])) {
2050 $local_url = $this->process_and_cache_manifest_image(
2051 $template['preview_url'],
2052 $kit_dir,
2053 $kit_id,
2054 "template-{$template_id}-preview"
2055 );
2056 if ($local_url) {
2057 $template['preview_url'] = $local_url;
2058 $updated = true;
2059 }
2060 }
2061 }
2062 }
2063
2064 // Process pages in manifest (alternative structure)
2065 if (isset($manifest['pages']) && is_array($manifest['pages'])) {
2066 foreach ($manifest['pages'] as &$page) {
2067 $page_id = $page['page_id'] ?? $page['id'] ?? uniqid();
2068
2069 // Process screenshot
2070 if (isset($page['screenshot'])) {
2071 $local_url = $this->process_and_cache_manifest_image(
2072 $page['screenshot'],
2073 $kit_dir,
2074 $kit_id,
2075 "page-{$page_id}-screenshot"
2076 );
2077 if ($local_url) {
2078 $page['screenshot'] = $local_url;
2079 $updated = true;
2080 }
2081 }
2082
2083 // Process thumbnail
2084 if (isset($page['thumbnail'])) {
2085 $local_url = $this->process_and_cache_manifest_image(
2086 $page['thumbnail'],
2087 $kit_dir,
2088 $kit_id,
2089 "page-{$page_id}-thumb"
2090 );
2091 if ($local_url) {
2092 $page['thumbnail'] = $local_url;
2093 $updated = true;
2094 }
2095 }
2096
2097 // Process preview_url
2098 if (isset($page['preview_url']) && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $page['preview_url'])) {
2099 $local_url = $this->process_and_cache_manifest_image(
2100 $page['preview_url'],
2101 $kit_dir,
2102 $kit_id,
2103 "page-{$page_id}-preview"
2104 );
2105 if ($local_url) {
2106 $page['preview_url'] = $local_url;
2107 $updated = true;
2108 }
2109 }
2110 }
2111 }
2112
2113 // Process images array in manifest
2114 if (isset($manifest['images']) && is_array($manifest['images'])) {
2115 foreach ($manifest['images'] as &$image) {
2116 // Process thumbnail_url
2117 if (isset($image['thumbnail_url'])) {
2118 $filename = $image['filename'] ?? uniqid();
2119 $local_url = $this->process_and_cache_manifest_image(
2120 $image['thumbnail_url'],
2121 $kit_dir,
2122 $kit_id,
2123 "image-" . pathinfo($filename, PATHINFO_FILENAME)
2124 );
2125 if ($local_url) {
2126 $image['thumbnail_url'] = $local_url;
2127 $updated = true;
2128 }
2129 }
2130
2131 // Process image_urls if it contains URLs
2132 if (isset($image['image_urls']) && !empty($image['image_urls'])) {
2133 if (filter_var($image['image_urls'], FILTER_VALIDATE_URL)) {
2134 $filename = $image['filename'] ?? uniqid();
2135 $local_url = $this->process_and_cache_manifest_image(
2136 $image['image_urls'],
2137 $kit_dir,
2138 $kit_id,
2139 "image-url-" . pathinfo($filename, PATHINFO_FILENAME)
2140 );
2141 if ($local_url) {
2142 $image['image_urls'] = $local_url;
2143 $updated = true;
2144 }
2145 }
2146 }
2147 }
2148 }
2149
2150 // If we made updates, save the manifest back
2151 if ($updated) {
2152 $json = wp_json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
2153 file_put_contents($manifest_file, $json);
2154 }
2155
2156 return true;
2157 }
2158
2159 /**
2160 * Process and cache an image from manifest
2161 * @param string $image_url The image URL (can be relative or absolute)
2162 * @param string $kit_dir The kit directory path
2163 * @param string $kit_id The kit ID
2164 * @param string $filename_prefix Prefix for the cached file
2165 * @return string|false Local URL or false on failure
2166 */
2167 private function process_and_cache_manifest_image($image_url, $kit_dir, $kit_id, $filename_prefix) {
2168 if (empty($image_url)) {
2169 return false;
2170 }
2171
2172 // Check if it's a relative path to screenshots folder
2173 if (strpos($image_url, 'screenshots/') === 0) {
2174 // It's already a local path in the kit directory
2175 $local_file = rtrim($kit_dir, '/') . '/' . $image_url; // Ensure no double slashes
2176 if (file_exists($local_file)) {
2177 // Convert to URL
2178 $upload_dir = wp_upload_dir();
2179 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2180 // Clean up any double slashes in the path (but preserve protocol ://)
2181 $url = $upload_dir['baseurl'] . $relative_path;
2182 $url = preg_replace('#(?<!:)//+#', '/', $url);
2183 return $url;
2184 }
2185 }
2186
2187 // If it's a full URL, download and cache it
2188 if (filter_var($image_url, FILTER_VALIDATE_URL)) {
2189 // Check if it's already a local URL pointing to our cache
2190 $upload_dir = wp_upload_dir();
2191 if (strpos($image_url, $upload_dir['baseurl']) === 0) {
2192 // Clean up any double slashes (but preserve protocol ://)
2193 return preg_replace('#(?<!:)//+#', '/', $image_url);
2194 }
2195
2196 // Determine if this is a purchased kit
2197 $is_purchased = strpos($kit_dir, '/purchased_kits/') !== false;
2198
2199 // Download and cache remote image - using proper directory
2200 if ($is_purchased) {
2201 // For purchased kits, save to purchased_kits/images folder
2202 $cache_base = $this->purchased_dir;
2203 $folder = 'images';
2204 $local_file = $cache_base . "{$folder}/{$filename_prefix}.jpg";
2205
2206 // Skip if already exists
2207 if (file_exists($local_file)) {
2208 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2209 return $upload_dir['baseurl'] . $relative_path;
2210 }
2211
2212 // Ensure folder exists
2213 if (!file_exists($cache_base . $folder)) {
2214 wp_mkdir_p($cache_base . $folder);
2215 }
2216
2217 // Download image
2218 $response = wp_remote_get($image_url, [
2219 'timeout' => 30,
2220 'sslverify' => false
2221 ]);
2222
2223 if (!is_wp_error($response)) {
2224 $image_data = wp_remote_retrieve_body($response);
2225 if (file_put_contents($local_file, $image_data)) {
2226 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2227 return $upload_dir['baseurl'] . $relative_path;
2228 }
2229 }
2230 } else {
2231 // For regular kits, use existing cache_image method
2232 $cached_url = $this->cache_image($image_url, $filename_prefix, 'thumbnails');
2233 if ($cached_url) {
2234 return $cached_url;
2235 }
2236 }
2237 }
2238
2239 // Check if image exists in screenshots folder and return its URL
2240 $screenshots_dir = rtrim($kit_dir, '/') . '/screenshots/';
2241 if (file_exists($screenshots_dir)) {
2242 // Extract filename from URL
2243 $filename = basename(parse_url($image_url, PHP_URL_PATH));
2244 $local_file = $screenshots_dir . $filename;
2245
2246 if (file_exists($local_file)) {
2247 // Convert to URL
2248 $upload_dir = wp_upload_dir();
2249 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2250 $url = $upload_dir['baseurl'] . $relative_path;
2251 return preg_replace('#(?<!:)//+#', '/', $url);
2252 }
2253 }
2254
2255 return false;
2256 }
2257
2258 /**
2259 * Process all template JSON files in the kit to replace remote URLs
2260 * @param string $kit_dir The kit directory path
2261 */
2262 private function process_template_json_files($kit_dir) {
2263 $templates_dir = rtrim($kit_dir, '/') . '/templates/';
2264
2265 if (!file_exists($templates_dir)) {
2266 return;
2267 }
2268
2269 // Get all JSON files in templates directory
2270 $json_files = glob($templates_dir . '*.json');
2271
2272 foreach ($json_files as $json_file) {
2273 $this->process_single_template_json($json_file, $kit_dir);
2274 }
2275 }
2276
2277 /**
2278 * Process a single template JSON file to replace remote image URLs
2279 * @param string $json_file Path to the JSON file
2280 * @param string $kit_dir The kit directory path
2281 */
2282 private function process_single_template_json($json_file, $kit_dir) {
2283 if (!file_exists($json_file)) {
2284 return;
2285 }
2286
2287 $content = file_get_contents($json_file);
2288 $data = json_decode($content, true);
2289
2290 if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
2291 return;
2292 }
2293
2294 $kit_id = basename($kit_dir);
2295 $template_name = basename($json_file, '.json');
2296 $updated = false;
2297
2298 // Process the content recursively
2299 $processed_data = $this->process_elementor_data_for_images($data, $kit_id, $template_name, $kit_dir);
2300
2301 if ($processed_data !== $data) {
2302 // Save the updated JSON
2303 $json = wp_json_encode($processed_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
2304 file_put_contents($json_file, $json);
2305 }
2306 }
2307
2308 /**
2309 * Process Elementor data recursively to replace image URLs
2310 * @param mixed $data The data to process
2311 * @param string $kit_id The kit ID
2312 * @param string $context Context for filename generation
2313 * @param string $kit_dir The kit directory path
2314 * @return mixed Processed data
2315 */
2316 private function process_elementor_data_for_images($data, $kit_id, $context, $kit_dir = '') {
2317 if (!is_array($data)) {
2318 return $data;
2319 }
2320
2321 foreach ($data as $key => &$value) {
2322 // Check for image-related fields
2323 if ($key === 'url' && is_string($value) && $this->is_image_url($value)) {
2324 // Process image URL
2325 $local_url = $this->download_and_replace_image($value, $kit_id, $context, $kit_dir);
2326 if ($local_url) {
2327 $value = $local_url;
2328 }
2329 } elseif (is_array($value)) {
2330 // Handle specific image structures
2331 if (isset($value['url']) && $this->is_image_url($value['url'])) {
2332 $local_url = $this->download_and_replace_image($value['url'], $kit_id, $context, $kit_dir);
2333 if ($local_url) {
2334 $value['url'] = $local_url;
2335 }
2336 }
2337
2338 // Recursively process nested arrays
2339 $value = $this->process_elementor_data_for_images($value, $kit_id, $context, $kit_dir);
2340 }
2341 }
2342
2343 return $data;
2344 }
2345
2346 /**
2347 * Check if a URL is an image URL
2348 * @param string $url The URL to check
2349 * @return bool
2350 */
2351 private function is_image_url($url) {
2352 if (!is_string($url)) {
2353 return false;
2354 }
2355
2356 // Check for image extensions
2357 if (preg_match('/\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i', $url)) {
2358 return true;
2359 }
2360
2361 // Check for WordPress uploads
2362 if (strpos($url, '/wp-content/uploads/') !== false) {
2363 return true;
2364 }
2365
2366 return false;
2367 }
2368
2369 /**
2370 * Download and replace an image URL with local cached version
2371 * @param string $image_url The image URL
2372 * @param string $kit_id The kit ID
2373 * @param string $context Context for filename
2374 * @param string $kit_dir Optional kit directory to determine if purchased
2375 * @return string|false Local URL or false on failure
2376 */
2377 private function download_and_replace_image($image_url, $kit_id, $context, $kit_dir = '') {
2378 if (empty($image_url) || !filter_var($image_url, FILTER_VALIDATE_URL)) {
2379 return false;
2380 }
2381
2382 // Check if it's already a local URL
2383 $upload_dir = wp_upload_dir();
2384 if (strpos($image_url, $upload_dir['baseurl']) === 0) {
2385 // Clean up any double slashes
2386 return preg_replace('#(?<!:)//+#', '/', $image_url);
2387 }
2388
2389 // Determine if this is a purchased kit
2390 $is_purchased = false;
2391 if (!empty($kit_dir) && strpos($kit_dir, '/purchased_kits/') !== false) {
2392 $is_purchased = true;
2393 }
2394
2395 // Generate filename
2396 $url_hash = md5($image_url);
2397 $filename = "kit-{$kit_id}-{$context}-{$url_hash}";
2398
2399 // Download and cache the image (it will use the correct directory based on is_purchased)
2400 $cache_base = $is_purchased ? $this->purchased_dir : $this->cache_dir;
2401 $folder = 'images';
2402 $local_file = $cache_base . "{$folder}/{$filename}.jpg";
2403
2404 // Skip downloading if already cached
2405 if (file_exists($local_file)) {
2406 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2407 return $upload_dir['baseurl'] . $relative_path;
2408 }
2409
2410 // Ensure the folder exists
2411 $folder_path = $cache_base . $folder;
2412 if (!file_exists($folder_path)) {
2413 wp_mkdir_p($folder_path);
2414 }
2415
2416 // Download the image
2417 $response = wp_remote_get($image_url, [
2418 'timeout' => 30,
2419 'sslverify' => false
2420 ]);
2421
2422 if (is_wp_error($response)) {
2423 return false;
2424 }
2425
2426 $image_data = wp_remote_retrieve_body($response);
2427
2428 // Detect actual extension from content.
2429 // Note: finfo_close() is deprecated in PHP 8.5 (finfo objects are
2430 // freed automatically). The local `$finfo` goes out of scope at
2431 // function return, so no explicit close is needed for GC in any
2432 // supported PHP version.
2433 $finfo = finfo_open(FILEINFO_MIME_TYPE);
2434 $mime_type = finfo_buffer($finfo, $image_data);
2435
2436 $extension = 'jpg';
2437 if ($mime_type === 'image/png') $extension = 'png';
2438 elseif ($mime_type === 'image/gif') $extension = 'gif';
2439 elseif ($mime_type === 'image/webp') $extension = 'webp';
2440
2441 $local_file = $cache_base . "{$folder}/{$filename}.{$extension}";
2442
2443 if (file_put_contents($local_file, $image_data)) {
2444 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2445 return $upload_dir['baseurl'] . $relative_path;
2446 }
2447
2448 return false;
2449 }
2450
2451 /**
2452 * Process nav_menu.json to remove remote site URLs
2453 * @param string $kit_dir The kit directory path
2454 */
2455 private function process_nav_menu_json($kit_dir) {
2456 $nav_menu_file = rtrim($kit_dir, '/') . '/nav_menu.json';
2457
2458 if (!file_exists($nav_menu_file)) {
2459 return;
2460 }
2461
2462 $content = file_get_contents($nav_menu_file);
2463 $data = json_decode($content, true);
2464
2465 if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
2466 return;
2467 }
2468
2469 $updated = false;
2470
2471 // Replace site_url with placeholder
2472 if (isset($data['site_url'])) {
2473 $data['site_url'] = '{{SITE_URL}}';
2474 $updated = true;
2475 }
2476
2477 // Process menu items to replace URLs
2478 if (isset($data['menus']) && is_array($data['menus'])) {
2479 foreach ($data['menus'] as &$menu) {
2480 if (isset($menu['items']) && is_array($menu['items'])) {
2481 foreach ($menu['items'] as &$item) {
2482 if (isset($item['url']) && filter_var($item['url'], FILTER_VALIDATE_URL)) {
2483 // Replace absolute URLs with relative or placeholder
2484 $parsed = parse_url($item['url']);
2485 if (isset($parsed['path'])) {
2486 // Use relative URL
2487 $item['url'] = $parsed['path'];
2488 if (isset($parsed['query'])) {
2489 $item['url'] .= '?' . $parsed['query'];
2490 }
2491 if (isset($parsed['fragment'])) {
2492 $item['url'] .= '#' . $parsed['fragment'];
2493 }
2494 $updated = true;
2495 }
2496 }
2497 }
2498 }
2499 }
2500 }
2501
2502 if ($updated) {
2503 // Save the updated nav_menu.json
2504 $json = wp_json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
2505 file_put_contents($nav_menu_file, $json);
2506 }
2507 }
2508
2509 /**
2510 * Get local screenshot URL for a template
2511 * @param string $kit_dir Kit directory path
2512 * @param array $template Template data
2513 * @param string $type Type of image (thumbnail or preview)
2514 * @return string|false Local URL or false if not found
2515 */
2516 private function get_local_screenshot_url($kit_dir, $template, $type = 'thumbnail') {
2517 // Check for screenshots folder
2518 $screenshots_dir = $kit_dir . '/screenshots/';
2519 if (!file_exists($screenshots_dir)) {
2520 return false;
2521 }
2522
2523 // Try to find the screenshot file
2524 $template_slug = $template['slug'] ?? $template['id'] ?? '';
2525 if (empty($template_slug)) {
2526 return false;
2527 }
2528
2529 // Common image extensions
2530 $extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
2531
2532 // Possible filename patterns
2533 $patterns = [
2534 $template_slug,
2535 $template_slug . '-' . $type,
2536 $type . '-' . $template_slug,
2537 str_replace('_', '-', $template_slug),
2538 str_replace('-', '_', $template_slug)
2539 ];
2540
2541 foreach ($patterns as $pattern) {
2542 foreach ($extensions as $ext) {
2543 $screenshot_file = $screenshots_dir . $pattern . '.' . $ext;
2544 if (file_exists($screenshot_file)) {
2545 // Convert to URL
2546 $upload_dir = wp_upload_dir();
2547 $relative_path = str_replace($upload_dir['basedir'], '', $screenshot_file);
2548 return $upload_dir['baseurl'] . $relative_path;
2549 }
2550 }
2551 }
2552
2553 // If type is thumbnail, also check without suffix
2554 if ($type === 'thumbnail') {
2555 foreach ($extensions as $ext) {
2556 $screenshot_file = $screenshots_dir . $template_slug . '.' . $ext;
2557 if (file_exists($screenshot_file)) {
2558 // Convert to URL
2559 $upload_dir = wp_upload_dir();
2560 $relative_path = str_replace($upload_dir['basedir'], '', $screenshot_file);
2561 return $upload_dir['baseurl'] . $relative_path;
2562 }
2563 }
2564 }
2565
2566 return false;
2567 }
2568
2569 /**
2570 * Get kit manifest from cache or purchased kits
2571 * @param string $kit_id The kit ID
2572 * @return array|false Manifest data or false
2573 */
2574 public function get_kit_manifest($kit_id) {
2575 // OPTIMIZATION: Use static cache for manifests
2576 static $manifest_cache = [];
2577
2578 if (isset($manifest_cache[$kit_id])) {
2579 return $manifest_cache[$kit_id];
2580 }
2581
2582 // First check if this is a purchased kit
2583 if ($this->is_kit_purchased($kit_id)) {
2584 // Check if manifest is already in purchased kits data
2585 $purchased_kits = $this->get_purchased_kits();
2586 foreach ($purchased_kits as $pk) {
2587 if (($pk['kit_id'] ?? '') === $kit_id && isset($pk['manifest'])) {
2588 $manifest_cache[$kit_id] = $pk['manifest'];
2589 $manifest_cache[$kit_id]['is_purchased'] = true;
2590 return $manifest_cache[$kit_id];
2591 }
2592 }
2593
2594 // First try to read the actual manifest.json file from purchased kit directory
2595 $purchased_kit_dir = $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '/';
2596 $manifest_file = $purchased_kit_dir . 'manifest.json';
2597
2598 if (file_exists($manifest_file)) {
2599 $content = file_get_contents($manifest_file);
2600 $data = json_decode($content, true);
2601 if (json_last_error() === JSON_ERROR_NONE) {
2602 // Add is_purchased flag
2603 $data['is_purchased'] = true;
2604 $manifest_cache[$kit_id] = $data;
2605 return $data;
2606 }
2607 }
2608
2609 // If no manifest file, try to get from stored data
2610 $purchased_kit = $this->get_purchased_kit($kit_id);
2611 if ($purchased_kit) {
2612 // Return manifest if available
2613 if (isset($purchased_kit['manifest'])) {
2614 $purchased_kit['manifest']['is_purchased'] = true;
2615 return $purchased_kit['manifest'];
2616 }
2617
2618 // Check if there's a kit_path with manifest
2619 if (isset($purchased_kit['kit_path'])) {
2620 $manifest_file = $purchased_kit['kit_path'] . '/manifest.json';
2621 if (file_exists($manifest_file)) {
2622 $content = file_get_contents($manifest_file);
2623 $data = json_decode($content, true);
2624 if (json_last_error() === JSON_ERROR_NONE) {
2625 $data['is_purchased'] = true;
2626 return $data;
2627 }
2628 }
2629 }
2630
2631 // Build manifest from purchased kit data
2632 return [
2633 'name' => $purchased_kit['kit_name'] ?? $purchased_kit['title'] ?? '',
2634 'title' => $purchased_kit['kit_name'] ?? $purchased_kit['title'] ?? '',
2635 'description' => $purchased_kit['description'] ?? '',
2636 'author' => $purchased_kit['author'] ?? '',
2637 'pages' => $purchased_kit['templates'] ?? [],
2638 'templates' => $purchased_kit['templates'] ?? [],
2639 'kit_id' => $kit_id,
2640 'is_purchased' => true,
2641 'required_plugins' => $purchased_kit['required_plugins'] ?? [],
2642 'requirements' => $purchased_kit['required_plugins'] ?? [] // Also include as requirements for compatibility
2643 ];
2644 }
2645 }
2646
2647 // Check regular cache
2648 $kit_dir = $this->cache_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2649 $manifest_file = $kit_dir . 'manifest.json';
2650
2651 if (file_exists($manifest_file)) {
2652 $content = file_get_contents($manifest_file);
2653 $data = json_decode($content, true);
2654 return json_last_error() === JSON_ERROR_NONE ? $data : false;
2655 }
2656
2657 return false;
2658 }
2659
2660 /**
2661 * Get kit template JSON from cache or purchased kits
2662 * @param string $kit_id The kit ID
2663 * @param string $template_name The template name/slug
2664 * @return array|false Template data or false
2665 */
2666 public function get_kit_template($kit_id, $template_name) {
2667 // First check if this is a purchased/uploaded kit
2668 if ($this->is_kit_purchased($kit_id)) {
2669 $purchased_kit = $this->get_purchased_kit($kit_id);
2670 if ($purchased_kit && isset($purchased_kit['templates'])) {
2671 foreach ($purchased_kit['templates'] as $template) {
2672 if ($template['slug'] === $template_name || $template['id'] === $template_name) {
2673 return $template;
2674 }
2675 }
2676 }
2677
2678 // If purchased kit has a path, check the file system
2679 if (isset($purchased_kit['kit_path'])) {
2680 $template_file = $purchased_kit['kit_path'] . '/' . sanitize_file_name($template_name) . '.json';
2681 if (file_exists($template_file)) {
2682 $content = file_get_contents($template_file);
2683 $data = json_decode($content, true);
2684 if (json_last_error() === JSON_ERROR_NONE) {
2685 return $data;
2686 }
2687 }
2688 }
2689
2690 // Check purchased kits directory
2691 $purchased_kit_dir = $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2692 $template_file = $purchased_kit_dir . sanitize_file_name($template_name) . '.json';
2693 if (file_exists($template_file)) {
2694 $content = file_get_contents($template_file);
2695 $data = json_decode($content, true);
2696 if (json_last_error() === JSON_ERROR_NONE) {
2697 return $data;
2698 }
2699 }
2700 }
2701
2702 // Check regular cache
2703 $kit_dir = $this->cache_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2704 $template_file = $kit_dir . sanitize_file_name($template_name) . '.json';
2705
2706 if (file_exists($template_file)) {
2707 $content = file_get_contents($template_file);
2708 $data = json_decode($content, true);
2709 return json_last_error() === JSON_ERROR_NONE ? $data : false;
2710 }
2711
2712 return false;
2713 }
2714
2715 /**
2716 * Check if kit is cached or purchased
2717 * @param string $kit_id The kit ID
2718 * @return bool
2719 */
2720 public function is_kit_cached($kit_id) {
2721 // First check if it's a purchased kit
2722 if ($this->is_kit_purchased($kit_id)) {
2723 return true;
2724 }
2725
2726 // Check regular cache
2727 $kit_dir = $this->cache_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2728 $meta_file = $kit_dir . 'meta.json';
2729
2730 return $this->is_cache_valid($meta_file);
2731 }
2732
2733 /**
2734 * Clear kit cache
2735 * @param string $kit_id The kit ID (optional, clears all if not provided)
2736 * @return bool
2737 */
2738 public function clear_kit_cache($kit_id = null) {
2739 if ($kit_id) {
2740 // Clear specific kit
2741 $kit_dir = $this->cache_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2742 if (file_exists($kit_dir)) {
2743 $this->delete_directory_contents($kit_dir);
2744 return rmdir($kit_dir);
2745 }
2746 } else {
2747 // Clear all kits
2748 $kits_dir = $this->cache_dir . 'kits/';
2749 if (file_exists($kits_dir)) {
2750 $this->delete_directory_contents($kits_dir);
2751 return true;
2752 }
2753 }
2754
2755 return false;
2756 }
2757
2758 /**
2759 * Maybe download kit if not already cached
2760 * @param string $kit_id The kit ID
2761 * @return bool
2762 */
2763 private function maybe_download_kit($kit_id) {
2764 // Check if kit is already cached
2765 if ($this->is_kit_cached($kit_id)) {
2766 return true;
2767 }
2768
2769 // Download the kit in background
2770 $result = $this->download_and_cache_kit($kit_id);
2771 return $result && $result['success'];
2772 }
2773
2774 /**
2775 * Download all available kits
2776 * @return array Download statistics
2777 */
2778 public function download_all_kits() {
2779 $stats = [
2780 'downloaded' => 0,
2781 'failed' => 0,
2782 'skipped' => 0,
2783 'total' => 0
2784 ];
2785
2786 // Get all kits from cache or API
2787 $all_kits = $this->get_cached_kits(false, 'all');
2788
2789 if (!is_array($all_kits)) {
2790 return $stats;
2791 }
2792
2793 // Process each category
2794 foreach ($all_kits as $category => $kits) {
2795 if (!is_array($kits)) {
2796 continue;
2797 }
2798
2799 foreach ($kits as $kit) {
2800 if (!isset($kit['kit_id'])) {
2801 continue;
2802 }
2803
2804 $stats['total']++;
2805
2806 // Check if already cached
2807 if ($this->is_kit_cached($kit['kit_id'])) {
2808 $stats['skipped']++;
2809 continue;
2810 }
2811
2812 // Try to download
2813 $result = $this->download_and_cache_kit($kit['kit_id']);
2814
2815 if ($result && $result['success']) {
2816 $stats['downloaded']++;
2817 } else {
2818 $stats['failed']++;
2819 }
2820
2821 // Add a small delay to avoid overwhelming the server
2822 if ($stats['downloaded'] % 5 === 0) {
2823 sleep(1);
2824 }
2825 }
2826 }
2827
2828 return $stats;
2829 }
2830
2831 /**
2832 * Cache all images from a template kit
2833 */
2834 public function cache_kit_all_images($kit_id, $templates = null) {
2835 if (!$templates) {
2836 // Try to get templates from cache
2837 $cached_data = $this->get_cached_kit_content($kit_id, 'all', false);
2838 if (!$cached_data) {
2839 return false;
2840 }
2841 $templates = $cached_data;
2842 }
2843
2844 $image_count = 0;
2845
2846 // Process each template
2847 foreach ($templates as $template) {
2848 if (isset($template['thumbnail'])) {
2849 $this->cache_image($template['thumbnail'], "kit-{$kit_id}-thumb-{$template['id']}", 'thumbnails');
2850 $image_count++;
2851 }
2852
2853 if (isset($template['content'])) {
2854 // Count images in content
2855 $image_count += $this->count_and_cache_content_images($template['content'], $kit_id);
2856 }
2857 }
2858
2859 return $image_count;
2860 }
2861
2862 /**
2863 * Count and cache images in content
2864 */
2865 private function count_and_cache_content_images($content, $kit_id) {
2866 if (is_string($content)) {
2867 $content = json_decode($content, true);
2868 }
2869
2870 if (!is_array($content)) {
2871 return 0;
2872 }
2873
2874 $count = 0;
2875
2876 // Process the content to find all image URLs
2877 $image_urls = $this->extract_image_urls_from_content($content);
2878
2879 foreach ($image_urls as $url) {
2880 $url_hash = md5($url);
2881 $filename = "kit-{$kit_id}-content-{$url_hash}";
2882 if ($this->cache_image($url, $filename, 'images')) {
2883 $count++;
2884 }
2885 }
2886
2887 return $count;
2888 }
2889
2890 /**
2891 * Extract all image URLs from Elementor content
2892 */
2893 private function extract_image_urls_from_content($content, &$urls = []) {
2894 if (!is_array($content)) {
2895 return $urls;
2896 }
2897
2898 foreach ($content as $element) {
2899 if (isset($element['settings']) && is_array($element['settings'])) {
2900 $this->extract_image_urls_from_settings($element['settings'], $urls);
2901 }
2902
2903 if (isset($element['elements']) && is_array($element['elements'])) {
2904 $this->extract_image_urls_from_content($element['elements'], $urls);
2905 }
2906 }
2907
2908 return array_unique($urls);
2909 }
2910
2911 /**
2912 * Extract image URLs from element settings
2913 */
2914 private function extract_image_urls_from_settings($settings, &$urls) {
2915 if (!is_array($settings)) {
2916 return;
2917 }
2918
2919 foreach ($settings as $key => $value) {
2920 if (is_array($value)) {
2921 // Check if it's an image array with URL
2922 if (isset($value['url']) && filter_var($value['url'], FILTER_VALIDATE_URL)) {
2923 $urls[] = $value['url'];
2924 }
2925 // Recursively check nested arrays
2926 $this->extract_image_urls_from_settings($value, $urls);
2927 } elseif (is_string($value)) {
2928 // Check if the string contains image URLs
2929 if (preg_match_all('/(https?:\/\/[^\s"]+\.(?:jpg|jpeg|png|gif|svg|webp))/i', $value, $matches)) {
2930 $urls = array_merge($urls, $matches[1]);
2931 }
2932 }
2933 }
2934 }
2935
2936 /**
2937 * Store a purchased template kit permanently
2938 * @param array $kit_data The kit data to store
2939 * @return bool Success status
2940 */
2941 public function store_purchased_kit($kit_data) {
2942 if (!file_exists($this->purchased_dir)) {
2943 wp_mkdir_p($this->purchased_dir);
2944
2945 // Create subdirectories
2946 $subdirs = ['kits', 'manifests', 'thumbnails', 'metadata'];
2947 foreach ($subdirs as $subdir) {
2948 $subdir_path = $this->purchased_dir . $subdir . '/';
2949 if (!file_exists($subdir_path)) {
2950 wp_mkdir_p($subdir_path);
2951 }
2952 }
2953 }
2954
2955 // Extract kit ID and normalize it
2956 $kit_id = $kit_data['kit_id'] ?? $kit_data['template_id'] ?? '';
2957 if (empty($kit_id)) {
2958 return false;
2959 }
2960
2961 // If kit_path is not provided but we have a kit_id, check if it exists in purchased_kits
2962 if (!isset($kit_data['kit_path']) && !empty($kit_id)) {
2963 $potential_path = $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '/';
2964 if (file_exists($potential_path)) {
2965 $kit_data['kit_path'] = $potential_path;
2966 }
2967 }
2968
2969 // Process manifest and templates to update URLs if kit_path is provided
2970 if (isset($kit_data['kit_path']) && file_exists($kit_data['kit_path'])) {
2971 $this->process_extracted_kit_manifest($kit_data['kit_path']);
2972 $this->process_template_json_files($kit_data['kit_path']);
2973 $this->process_nav_menu_json($kit_data['kit_path']);
2974
2975 // Re-read the manifest after processing
2976 $manifest_file = $kit_data['kit_path'] . '/manifest.json';
2977 if (file_exists($manifest_file)) {
2978 $content = file_get_contents($manifest_file);
2979 $manifest = json_decode($content, true);
2980 if ($manifest) {
2981 $kit_data['manifest'] = $manifest;
2982
2983 // Update templates from processed manifest
2984 if (isset($manifest['templates'])) {
2985 $kit_data['templates'] = $manifest['templates'];
2986 } elseif (isset($manifest['pages'])) {
2987 $kit_data['templates'] = $manifest['pages'];
2988 }
2989 }
2990 }
2991 }
2992
2993 // IMPORTANT: Process URLs FIRST before creating metadata to ensure NO remote URLs are saved
2994 $processed_kit_data = $this->process_kit_data_urls($kit_data, $kit_id);
2995
2996 // Store kit metadata with same structure as cached kits - using PROCESSED data
2997 $metadata = [
2998 'kit_id' => $kit_id,
2999 'kit_name' => $processed_kit_data['kit_name'] ?? $processed_kit_data['title'] ?? '',
3000 'purchased_date' => current_time('mysql'),
3001 'is_purchased' => true,
3002 'purchasable' => false,
3003 'downloadable' => true,
3004 'is_pro' => false, // No restrictions for purchased templates
3005 'categories' => $processed_kit_data['categories'] ?? ['purchased'],
3006 'keywords' => $processed_kit_data['keywords'] ?? [],
3007 'thumbnail' => $processed_kit_data['thumbnail'] ?? '', // FIXED: Use processed thumbnail (local URL)
3008 'preview_url' => $processed_kit_data['preview_url'] ?? '', // FIXED: Use processed preview URL
3009 'descriptions' => $processed_kit_data['descriptions'] ?? $processed_kit_data['description'] ?? '',
3010 'downloads' => $processed_kit_data['downloads'] ?? 0,
3011 'purchase_url' => '', // Empty since already purchased
3012 'template_count' => isset($processed_kit_data['templates']) ? count($processed_kit_data['templates']) : 1,
3013 'required_plugins' => $processed_kit_data['required_plugins'] ?? [], // Store required plugins
3014 'kit_path' => $processed_kit_data['kit_path'] ?? null // Store the path if available
3015 ];
3016
3017 // Update metadata with processed manifest data if available
3018 if (isset($processed_kit_data['manifest'])) {
3019 // Preserve the full processed manifest (with local URLs)
3020 $metadata['manifest'] = $processed_kit_data['manifest'];
3021
3022 // Also extract required_plugins at the top level for easy access
3023 if (isset($processed_kit_data['manifest']['required_plugins'])) {
3024 $metadata['required_plugins'] = $processed_kit_data['manifest']['required_plugins'];
3025 }
3026 }
3027
3028 // Save metadata with processed URLs
3029 $metadata_file = $this->purchased_dir . 'metadata/' . sanitize_file_name($kit_id) . '.json';
3030 $this->write_cache_file($metadata_file, $metadata);
3031
3032 // Save full kit data if provided (already processed)
3033 if (isset($processed_kit_data['content']) || isset($processed_kit_data['templates']) || isset($processed_kit_data['manifest'])) {
3034 $kit_file = $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '.json';
3035 $this->write_cache_file($kit_file, $processed_kit_data);
3036 }
3037
3038 return true;
3039 }
3040
3041 /**
3042 * Update existing purchased kits to remove remote URLs and use local URLs
3043 * @return int Number of kits updated
3044 */
3045 public function update_existing_purchased_kits_urls() {
3046 if (!file_exists($this->purchased_dir . 'metadata/')) {
3047 return 0;
3048 }
3049
3050 $updated_count = 0;
3051 $metadata_files = glob($this->purchased_dir . 'metadata/*.json');
3052
3053 foreach ($metadata_files as $metadata_file) {
3054 $metadata = $this->read_cache_file($metadata_file);
3055 if (!$metadata) {
3056 continue;
3057 }
3058
3059 $kit_id = $metadata['kit_id'] ?? '';
3060 if (empty($kit_id)) {
3061 continue;
3062 }
3063
3064 $has_remote_urls = false;
3065
3066 // Check if metadata has remote URLs
3067 if (isset($metadata['thumbnail']) && strpos($metadata['thumbnail'], 'http') === 0) {
3068 $has_remote_urls = true;
3069 }
3070 if (isset($metadata['preview_url']) && strpos($metadata['preview_url'], 'http') === 0) {
3071 $has_remote_urls = true;
3072 }
3073 if (isset($metadata['manifest']['thumbnail']) && strpos($metadata['manifest']['thumbnail'], 'http') === 0) {
3074 $has_remote_urls = true;
3075 }
3076 if (isset($metadata['manifest']['thumbnail_url']) && strpos($metadata['manifest']['thumbnail_url'], 'http') === 0) {
3077 $has_remote_urls = true;
3078 }
3079
3080 if ($has_remote_urls) {
3081 // Process the metadata to convert remote URLs to local
3082 $processed_metadata = $this->process_kit_data_urls($metadata, $kit_id);
3083
3084 // Save the updated metadata
3085 $this->write_cache_file($metadata_file, $processed_metadata);
3086
3087 // Also update the kit data file if it exists
3088 $kit_file = $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '.json';
3089 if (file_exists($kit_file)) {
3090 $kit_data = $this->read_cache_file($kit_file);
3091 if ($kit_data) {
3092 $processed_kit_data = $this->process_kit_data_urls($kit_data, $kit_id);
3093 $this->write_cache_file($kit_file, $processed_kit_data);
3094 }
3095 }
3096
3097 $updated_count++;
3098 }
3099 }
3100
3101 return $updated_count;
3102 }
3103
3104 /**
3105 * Get all purchased kits
3106 * @return array Array of purchased kits
3107 */
3108 public function get_purchased_kits() {
3109 static $cached_purchased_kits = null;
3110
3111 if ($cached_purchased_kits !== null) {
3112 return $cached_purchased_kits;
3113 }
3114
3115 if (!file_exists($this->purchased_dir . 'metadata/')) {
3116 $cached_purchased_kits = [];
3117 return [];
3118 }
3119
3120 $purchased_kits = [];
3121 $metadata_files = glob($this->purchased_dir . 'metadata/*.json');
3122
3123 // OPTIMIZATION: Batch read all metadata files
3124 foreach ($metadata_files as $file) {
3125 $metadata = $this->read_cache_file($file);
3126 if ($metadata) {
3127 // Pre-load manifest data if available in the same directory
3128 $kit_id = $metadata['kit_id'] ?? '';
3129 if ($kit_id) {
3130 // Check if manifest.json exists in the kit directory
3131 $manifest_file = $this->purchased_dir . 'kits/kit_' . $kit_id . '/manifest.json';
3132 if (file_exists($manifest_file) && !isset($metadata['manifest'])) {
3133 $manifest_content = @file_get_contents($manifest_file);
3134 if ($manifest_content) {
3135 $manifest_data = json_decode($manifest_content, true);
3136 if ($manifest_data) {
3137 $metadata['manifest'] = $manifest_data;
3138 }
3139 }
3140 }
3141 }
3142 $purchased_kits[] = $metadata;
3143 }
3144 }
3145
3146 $cached_purchased_kits = $purchased_kits;
3147 return $purchased_kits;
3148 }
3149
3150 /**
3151 * Check if a kit is purchased
3152 * @param string $kit_id The kit ID to check
3153 * @return bool
3154 */
3155 public function is_kit_purchased($kit_id) {
3156 // Check with exact kit_id
3157 $metadata_file = $this->purchased_dir . 'metadata/' . sanitize_file_name($kit_id) . '.json';
3158 if (file_exists($metadata_file)) {
3159 return true;
3160 }
3161
3162 // Also check with kit_ prefix format for compatibility
3163 $metadata_file_alt = $this->purchased_dir . 'metadata/kit_' . sanitize_file_name($kit_id) . '.json';
3164 if (file_exists($metadata_file_alt)) {
3165 return true;
3166 }
3167
3168 // Check if the kit folder exists directly
3169 $kit_dir = $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '/';
3170 return file_exists($kit_dir);
3171 }
3172
3173 /**
3174 * Get purchased kit data
3175 * @param string $kit_id The kit ID
3176 * @return array|false Kit data or false if not found
3177 */
3178 public function get_purchased_kit($kit_id) {
3179 // Try direct metadata file first
3180 $metadata_file = $this->purchased_dir . 'metadata/' . sanitize_file_name($kit_id) . '.json';
3181 if (file_exists($metadata_file)) {
3182 return $this->read_cache_file($metadata_file);
3183 }
3184
3185 // Try with kit_ prefix
3186 $metadata_file_alt = $this->purchased_dir . 'metadata/kit_' . sanitize_file_name($kit_id) . '.json';
3187 if (file_exists($metadata_file_alt)) {
3188 return $this->read_cache_file($metadata_file_alt);
3189 }
3190
3191 // Try kits directory with stored data
3192 $kit_file = $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '.json';
3193 if (file_exists($kit_file)) {
3194 return $this->read_cache_file($kit_file);
3195 }
3196
3197 // Try kits directory with kit_ prefix
3198 $kit_file_alt = $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '.json';
3199 if (file_exists($kit_file_alt)) {
3200 return $this->read_cache_file($kit_file_alt);
3201 }
3202
3203 return false;
3204 }
3205
3206 /**
3207 * Delete a purchased kit (if needed for management)
3208 * @param string $kit_id The kit ID to delete
3209 * @return bool
3210 */
3211 public function delete_purchased_kit($kit_id) {
3212 $deleted = false;
3213
3214 // Delete metadata files (try both with and without kit_ prefix)
3215 $metadata_files = [
3216 $this->purchased_dir . 'metadata/' . sanitize_file_name($kit_id) . '.json',
3217 $this->purchased_dir . 'metadata/kit_' . sanitize_file_name($kit_id) . '.json'
3218 ];
3219
3220 foreach ($metadata_files as $metadata_file) {
3221 if (file_exists($metadata_file)) {
3222 @unlink($metadata_file);
3223 $deleted = true;
3224 }
3225 }
3226
3227 // Delete kit data files
3228 $kit_files = [
3229 $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '.json',
3230 $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '.json'
3231 ];
3232
3233 foreach ($kit_files as $kit_file) {
3234 if (file_exists($kit_file)) {
3235 @unlink($kit_file);
3236 $deleted = true;
3237 }
3238 }
3239
3240 // Delete kit directory and all its contents
3241 $kit_directories = [
3242 $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id),
3243 $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id)
3244 ];
3245
3246 foreach ($kit_directories as $kit_dir) {
3247 if (file_exists($kit_dir) && is_dir($kit_dir)) {
3248 // Delete all contents recursively
3249 $this->delete_directory_contents($kit_dir);
3250 @rmdir($kit_dir);
3251 $deleted = true;
3252 }
3253 }
3254
3255 // Clear any transient cache related to this kit
3256 delete_transient('jltma_kit_' . $kit_id);
3257 delete_transient('jltma_kit_templates_' . $kit_id);
3258 delete_transient('jltma_kit_manifest_' . $kit_id);
3259
3260 return $deleted;
3261 }
3262
3263 /**
3264 * Get singleton instance
3265 */
3266 public static function get_instance()
3267 {
3268 if (self::$instance === null) {
3269 self::$instance = new self();
3270 }
3271 return self::$instance;
3272 }
3273 }
3274
3275 // Initialize template kit cache manager
3276 Template_Kit_Cache::get_instance();