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

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