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

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

3,297 lines 119.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 // ZipArchive::extractTo() does not validate entry paths, so a
1936 // crafted archive could write outside $destination ("Zip Slip").
1937 // Validate every entry and only extract the explicit safe list;
1938 // bail out entirely if any entry is absolute or traverses upward.
1939 $safe_entries = array();
1940 for ($i = 0; $i < $zip->numFiles; $i++) {
1941 $entry = $zip->getNameIndex($i);
1942 if (false === $entry) {
1943 continue;
1944 }
1945 $normalized = str_replace('\\', '/', $entry);
1946 if (false !== strpos($entry, "\0")
1947 || preg_match('#(^/|^[A-Za-z]:|(^|/)\.\.(/|$))#', $normalized)) {
1948 $zip->close();
1949 return false;
1950 }
1951 $safe_entries[] = $entry;
1952 }
1953
1954 $zip->extractTo($destination, $safe_entries);
1955 $zip->close();
1956
1957 // Process manifest after extraction
1958 $this->process_extracted_kit_manifest($destination);
1959 return true;
1960 }
1961 }
1962 return false;
1963 }
1964
1965 // Process manifest after successful extraction
1966 $this->process_extracted_kit_manifest($destination);
1967
1968 // Process template JSON files
1969 $this->process_template_json_files($destination);
1970
1971 // Process nav_menu.json
1972 $this->process_nav_menu_json($destination);
1973
1974 return true;
1975 }
1976
1977 /**
1978 * Process extracted kit manifest to update all image URLs to local paths
1979 * @param string $kit_dir Path to the extracted kit directory
1980 * @return bool Success status
1981 */
1982 private function process_extracted_kit_manifest($kit_dir) {
1983 $manifest_file = $kit_dir . '/manifest.json';
1984
1985 if (!file_exists($manifest_file)) {
1986 return false;
1987 }
1988
1989 // Read the manifest
1990 $content = file_get_contents($manifest_file);
1991 $manifest = json_decode($content, true);
1992
1993 if (json_last_error() !== JSON_ERROR_NONE || !is_array($manifest)) {
1994 return false;
1995 }
1996
1997 // Extract kit_id from directory name or manifest
1998 $kit_id = basename($kit_dir);
1999 if (isset($manifest['kit_id'])) {
2000 $kit_id = $manifest['kit_id'];
2001 }
2002
2003 $updated = false;
2004
2005 // Process main kit thumbnail if exists
2006 if (isset($manifest['thumbnail'])) {
2007 $local_url = $this->process_and_cache_manifest_image($manifest['thumbnail'], $kit_dir, $kit_id, 'kit-main');
2008 if ($local_url) {
2009 $manifest['thumbnail'] = $local_url;
2010 $updated = true;
2011 }
2012 }
2013
2014 // Process thumbnail_url if exists
2015 if (isset($manifest['thumbnail_url'])) {
2016 $local_url = $this->process_and_cache_manifest_image($manifest['thumbnail_url'], $kit_dir, $kit_id, 'kit-thumb');
2017 if ($local_url) {
2018 $manifest['thumbnail_url'] = $local_url;
2019 $updated = true;
2020 }
2021 }
2022
2023 // Process preview_url if exists
2024 if (isset($manifest['preview_url'])) {
2025 // Preview URLs are usually live sites, so we might not want to cache them
2026 // But if it's an image URL, we can cache it
2027 if (preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $manifest['preview_url'])) {
2028 $local_url = $this->process_and_cache_manifest_image($manifest['preview_url'], $kit_dir, $kit_id, 'kit-preview');
2029 if ($local_url) {
2030 $manifest['preview_url'] = $local_url;
2031 $updated = true;
2032 }
2033 }
2034 }
2035
2036 // Process templates in manifest
2037 if (isset($manifest['templates']) && is_array($manifest['templates'])) {
2038 foreach ($manifest['templates'] as &$template) {
2039 $template_id = $template['template_id'] ?? $template['id'] ?? uniqid();
2040
2041 // Process screenshot
2042 if (isset($template['screenshot'])) {
2043 $local_url = $this->process_and_cache_manifest_image(
2044 $template['screenshot'],
2045 $kit_dir,
2046 $kit_id,
2047 "template-{$template_id}-screenshot"
2048 );
2049 if ($local_url) {
2050 $template['screenshot'] = $local_url;
2051 $updated = true;
2052 }
2053 }
2054
2055 // Process thumbnail
2056 if (isset($template['thumbnail'])) {
2057 $local_url = $this->process_and_cache_manifest_image(
2058 $template['thumbnail'],
2059 $kit_dir,
2060 $kit_id,
2061 "template-{$template_id}-thumb"
2062 );
2063 if ($local_url) {
2064 $template['thumbnail'] = $local_url;
2065 $updated = true;
2066 }
2067 }
2068
2069 // Process preview_url
2070 if (isset($template['preview_url']) && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $template['preview_url'])) {
2071 $local_url = $this->process_and_cache_manifest_image(
2072 $template['preview_url'],
2073 $kit_dir,
2074 $kit_id,
2075 "template-{$template_id}-preview"
2076 );
2077 if ($local_url) {
2078 $template['preview_url'] = $local_url;
2079 $updated = true;
2080 }
2081 }
2082 }
2083 }
2084
2085 // Process pages in manifest (alternative structure)
2086 if (isset($manifest['pages']) && is_array($manifest['pages'])) {
2087 foreach ($manifest['pages'] as &$page) {
2088 $page_id = $page['page_id'] ?? $page['id'] ?? uniqid();
2089
2090 // Process screenshot
2091 if (isset($page['screenshot'])) {
2092 $local_url = $this->process_and_cache_manifest_image(
2093 $page['screenshot'],
2094 $kit_dir,
2095 $kit_id,
2096 "page-{$page_id}-screenshot"
2097 );
2098 if ($local_url) {
2099 $page['screenshot'] = $local_url;
2100 $updated = true;
2101 }
2102 }
2103
2104 // Process thumbnail
2105 if (isset($page['thumbnail'])) {
2106 $local_url = $this->process_and_cache_manifest_image(
2107 $page['thumbnail'],
2108 $kit_dir,
2109 $kit_id,
2110 "page-{$page_id}-thumb"
2111 );
2112 if ($local_url) {
2113 $page['thumbnail'] = $local_url;
2114 $updated = true;
2115 }
2116 }
2117
2118 // Process preview_url
2119 if (isset($page['preview_url']) && preg_match('/\.(jpg|jpeg|png|gif|webp)$/i', $page['preview_url'])) {
2120 $local_url = $this->process_and_cache_manifest_image(
2121 $page['preview_url'],
2122 $kit_dir,
2123 $kit_id,
2124 "page-{$page_id}-preview"
2125 );
2126 if ($local_url) {
2127 $page['preview_url'] = $local_url;
2128 $updated = true;
2129 }
2130 }
2131 }
2132 }
2133
2134 // Process images array in manifest
2135 if (isset($manifest['images']) && is_array($manifest['images'])) {
2136 foreach ($manifest['images'] as &$image) {
2137 // Process thumbnail_url
2138 if (isset($image['thumbnail_url'])) {
2139 $filename = $image['filename'] ?? uniqid();
2140 $local_url = $this->process_and_cache_manifest_image(
2141 $image['thumbnail_url'],
2142 $kit_dir,
2143 $kit_id,
2144 "image-" . pathinfo($filename, PATHINFO_FILENAME)
2145 );
2146 if ($local_url) {
2147 $image['thumbnail_url'] = $local_url;
2148 $updated = true;
2149 }
2150 }
2151
2152 // Process image_urls if it contains URLs
2153 if (isset($image['image_urls']) && !empty($image['image_urls'])) {
2154 if (filter_var($image['image_urls'], FILTER_VALIDATE_URL)) {
2155 $filename = $image['filename'] ?? uniqid();
2156 $local_url = $this->process_and_cache_manifest_image(
2157 $image['image_urls'],
2158 $kit_dir,
2159 $kit_id,
2160 "image-url-" . pathinfo($filename, PATHINFO_FILENAME)
2161 );
2162 if ($local_url) {
2163 $image['image_urls'] = $local_url;
2164 $updated = true;
2165 }
2166 }
2167 }
2168 }
2169 }
2170
2171 // If we made updates, save the manifest back
2172 if ($updated) {
2173 $json = wp_json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
2174 file_put_contents($manifest_file, $json);
2175 }
2176
2177 return true;
2178 }
2179
2180 /**
2181 * Process and cache an image from manifest
2182 * @param string $image_url The image URL (can be relative or absolute)
2183 * @param string $kit_dir The kit directory path
2184 * @param string $kit_id The kit ID
2185 * @param string $filename_prefix Prefix for the cached file
2186 * @return string|false Local URL or false on failure
2187 */
2188 private function process_and_cache_manifest_image($image_url, $kit_dir, $kit_id, $filename_prefix) {
2189 if (empty($image_url)) {
2190 return false;
2191 }
2192
2193 // Check if it's a relative path to screenshots folder
2194 if (strpos($image_url, 'screenshots/') === 0) {
2195 // It's already a local path in the kit directory
2196 $local_file = rtrim($kit_dir, '/') . '/' . $image_url; // Ensure no double slashes
2197 if (file_exists($local_file)) {
2198 // Convert to URL
2199 $upload_dir = wp_upload_dir();
2200 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2201 // Clean up any double slashes in the path (but preserve protocol ://)
2202 $url = $upload_dir['baseurl'] . $relative_path;
2203 $url = preg_replace('#(?<!:)//+#', '/', $url);
2204 return $url;
2205 }
2206 }
2207
2208 // If it's a full URL, download and cache it
2209 if (filter_var($image_url, FILTER_VALIDATE_URL)) {
2210 // Check if it's already a local URL pointing to our cache
2211 $upload_dir = wp_upload_dir();
2212 if (strpos($image_url, $upload_dir['baseurl']) === 0) {
2213 // Clean up any double slashes (but preserve protocol ://)
2214 return preg_replace('#(?<!:)//+#', '/', $image_url);
2215 }
2216
2217 // Determine if this is a purchased kit
2218 $is_purchased = strpos($kit_dir, '/purchased_kits/') !== false;
2219
2220 // Download and cache remote image - using proper directory
2221 if ($is_purchased) {
2222 // For purchased kits, save to purchased_kits/images folder
2223 $cache_base = $this->purchased_dir;
2224 $folder = 'images';
2225 $local_file = $cache_base . "{$folder}/{$filename_prefix}.jpg";
2226
2227 // Skip if already exists
2228 if (file_exists($local_file)) {
2229 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2230 return $upload_dir['baseurl'] . $relative_path;
2231 }
2232
2233 // Ensure folder exists
2234 if (!file_exists($cache_base . $folder)) {
2235 wp_mkdir_p($cache_base . $folder);
2236 }
2237
2238 // Download image
2239 $response = wp_remote_get($image_url, [
2240 'timeout' => 30,
2241 'sslverify' => false
2242 ]);
2243
2244 if (!is_wp_error($response)) {
2245 $image_data = wp_remote_retrieve_body($response);
2246 if (file_put_contents($local_file, $image_data)) {
2247 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2248 return $upload_dir['baseurl'] . $relative_path;
2249 }
2250 }
2251 } else {
2252 // For regular kits, use existing cache_image method
2253 $cached_url = $this->cache_image($image_url, $filename_prefix, 'thumbnails');
2254 if ($cached_url) {
2255 return $cached_url;
2256 }
2257 }
2258 }
2259
2260 // Check if image exists in screenshots folder and return its URL
2261 $screenshots_dir = rtrim($kit_dir, '/') . '/screenshots/';
2262 if (file_exists($screenshots_dir)) {
2263 // Extract filename from URL
2264 $filename = basename(wp_parse_url($image_url, PHP_URL_PATH));
2265 $local_file = $screenshots_dir . $filename;
2266
2267 if (file_exists($local_file)) {
2268 // Convert to URL
2269 $upload_dir = wp_upload_dir();
2270 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2271 $url = $upload_dir['baseurl'] . $relative_path;
2272 return preg_replace('#(?<!:)//+#', '/', $url);
2273 }
2274 }
2275
2276 return false;
2277 }
2278
2279 /**
2280 * Process all template JSON files in the kit to replace remote URLs
2281 * @param string $kit_dir The kit directory path
2282 */
2283 private function process_template_json_files($kit_dir) {
2284 $templates_dir = rtrim($kit_dir, '/') . '/templates/';
2285
2286 if (!file_exists($templates_dir)) {
2287 return;
2288 }
2289
2290 // Get all JSON files in templates directory
2291 $json_files = glob($templates_dir . '*.json');
2292
2293 foreach ($json_files as $json_file) {
2294 $this->process_single_template_json($json_file, $kit_dir);
2295 }
2296 }
2297
2298 /**
2299 * Process a single template JSON file to replace remote image URLs
2300 * @param string $json_file Path to the JSON file
2301 * @param string $kit_dir The kit directory path
2302 */
2303 private function process_single_template_json($json_file, $kit_dir) {
2304 if (!file_exists($json_file)) {
2305 return;
2306 }
2307
2308 $content = file_get_contents($json_file);
2309 $data = json_decode($content, true);
2310
2311 if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
2312 return;
2313 }
2314
2315 $kit_id = basename($kit_dir);
2316 $template_name = basename($json_file, '.json');
2317 $updated = false;
2318
2319 // Process the content recursively
2320 $processed_data = $this->process_elementor_data_for_images($data, $kit_id, $template_name, $kit_dir);
2321
2322 if ($processed_data !== $data) {
2323 // Save the updated JSON
2324 $json = wp_json_encode($processed_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
2325 file_put_contents($json_file, $json);
2326 }
2327 }
2328
2329 /**
2330 * Process Elementor data recursively to replace image URLs
2331 * @param mixed $data The data to process
2332 * @param string $kit_id The kit ID
2333 * @param string $context Context for filename generation
2334 * @param string $kit_dir The kit directory path
2335 * @return mixed Processed data
2336 */
2337 private function process_elementor_data_for_images($data, $kit_id, $context, $kit_dir = '') {
2338 if (!is_array($data)) {
2339 return $data;
2340 }
2341
2342 foreach ($data as $key => &$value) {
2343 // Check for image-related fields
2344 if ($key === 'url' && is_string($value) && $this->is_image_url($value)) {
2345 // Process image URL
2346 $local_url = $this->download_and_replace_image($value, $kit_id, $context, $kit_dir);
2347 if ($local_url) {
2348 $value = $local_url;
2349 }
2350 } elseif (is_array($value)) {
2351 // Handle specific image structures
2352 if (isset($value['url']) && $this->is_image_url($value['url'])) {
2353 $local_url = $this->download_and_replace_image($value['url'], $kit_id, $context, $kit_dir);
2354 if ($local_url) {
2355 $value['url'] = $local_url;
2356 }
2357 }
2358
2359 // Recursively process nested arrays
2360 $value = $this->process_elementor_data_for_images($value, $kit_id, $context, $kit_dir);
2361 }
2362 }
2363
2364 return $data;
2365 }
2366
2367 /**
2368 * Check if a URL is an image URL
2369 * @param string $url The URL to check
2370 * @return bool
2371 */
2372 private function is_image_url($url) {
2373 if (!is_string($url)) {
2374 return false;
2375 }
2376
2377 // Check for image extensions
2378 if (preg_match('/\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)(\?.*)?$/i', $url)) {
2379 return true;
2380 }
2381
2382 // Check for WordPress uploads
2383 if (strpos($url, '/wp-content/uploads/') !== false) {
2384 return true;
2385 }
2386
2387 return false;
2388 }
2389
2390 /**
2391 * Download and replace an image URL with local cached version
2392 * @param string $image_url The image URL
2393 * @param string $kit_id The kit ID
2394 * @param string $context Context for filename
2395 * @param string $kit_dir Optional kit directory to determine if purchased
2396 * @return string|false Local URL or false on failure
2397 */
2398 private function download_and_replace_image($image_url, $kit_id, $context, $kit_dir = '') {
2399 if (empty($image_url) || !filter_var($image_url, FILTER_VALIDATE_URL)) {
2400 return false;
2401 }
2402
2403 // Check if it's already a local URL
2404 $upload_dir = wp_upload_dir();
2405 if (strpos($image_url, $upload_dir['baseurl']) === 0) {
2406 // Clean up any double slashes
2407 return preg_replace('#(?<!:)//+#', '/', $image_url);
2408 }
2409
2410 // Determine if this is a purchased kit
2411 $is_purchased = false;
2412 if (!empty($kit_dir) && strpos($kit_dir, '/purchased_kits/') !== false) {
2413 $is_purchased = true;
2414 }
2415
2416 // Generate filename
2417 $url_hash = md5($image_url);
2418 $filename = "kit-{$kit_id}-{$context}-{$url_hash}";
2419
2420 // Download and cache the image (it will use the correct directory based on is_purchased)
2421 $cache_base = $is_purchased ? $this->purchased_dir : $this->cache_dir;
2422 $folder = 'images';
2423 $local_file = $cache_base . "{$folder}/{$filename}.jpg";
2424
2425 // Skip downloading if already cached
2426 if (file_exists($local_file)) {
2427 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2428 return $upload_dir['baseurl'] . $relative_path;
2429 }
2430
2431 // Ensure the folder exists
2432 $folder_path = $cache_base . $folder;
2433 if (!file_exists($folder_path)) {
2434 wp_mkdir_p($folder_path);
2435 }
2436
2437 // Download the image
2438 $response = wp_remote_get($image_url, [
2439 'timeout' => 30,
2440 'sslverify' => false
2441 ]);
2442
2443 if (is_wp_error($response)) {
2444 return false;
2445 }
2446
2447 $image_data = wp_remote_retrieve_body($response);
2448
2449 // Detect actual extension from content.
2450 // Note: finfo_close() is deprecated in PHP 8.5 (finfo objects are
2451 // freed automatically). The local `$finfo` goes out of scope at
2452 // function return, so no explicit close is needed for GC in any
2453 // supported PHP version.
2454 $finfo = finfo_open(FILEINFO_MIME_TYPE);
2455 $mime_type = finfo_buffer($finfo, $image_data);
2456
2457 $extension = 'jpg';
2458 if ($mime_type === 'image/png') $extension = 'png';
2459 elseif ($mime_type === 'image/gif') $extension = 'gif';
2460 elseif ($mime_type === 'image/webp') $extension = 'webp';
2461
2462 $local_file = $cache_base . "{$folder}/{$filename}.{$extension}";
2463
2464 if (file_put_contents($local_file, $image_data)) {
2465 $relative_path = str_replace($upload_dir['basedir'], '', $local_file);
2466 return $upload_dir['baseurl'] . $relative_path;
2467 }
2468
2469 return false;
2470 }
2471
2472 /**
2473 * Process nav_menu.json to remove remote site URLs
2474 * @param string $kit_dir The kit directory path
2475 */
2476 private function process_nav_menu_json($kit_dir) {
2477 $nav_menu_file = rtrim($kit_dir, '/') . '/nav_menu.json';
2478
2479 if (!file_exists($nav_menu_file)) {
2480 return;
2481 }
2482
2483 $content = file_get_contents($nav_menu_file);
2484 $data = json_decode($content, true);
2485
2486 if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) {
2487 return;
2488 }
2489
2490 $updated = false;
2491
2492 // Replace site_url with placeholder
2493 if (isset($data['site_url'])) {
2494 $data['site_url'] = '{{SITE_URL}}';
2495 $updated = true;
2496 }
2497
2498 // Process menu items to replace URLs
2499 if (isset($data['menus']) && is_array($data['menus'])) {
2500 foreach ($data['menus'] as &$menu) {
2501 if (isset($menu['items']) && is_array($menu['items'])) {
2502 foreach ($menu['items'] as &$item) {
2503 if (isset($item['url']) && filter_var($item['url'], FILTER_VALIDATE_URL)) {
2504 // Replace absolute URLs with relative or placeholder
2505 $parsed = wp_parse_url($item['url']);
2506 if (isset($parsed['path'])) {
2507 // Use relative URL
2508 $item['url'] = $parsed['path'];
2509 if (isset($parsed['query'])) {
2510 $item['url'] .= '?' . $parsed['query'];
2511 }
2512 if (isset($parsed['fragment'])) {
2513 $item['url'] .= '#' . $parsed['fragment'];
2514 }
2515 $updated = true;
2516 }
2517 }
2518 }
2519 }
2520 }
2521 }
2522
2523 if ($updated) {
2524 // Save the updated nav_menu.json
2525 $json = wp_json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
2526 file_put_contents($nav_menu_file, $json);
2527 }
2528 }
2529
2530 /**
2531 * Get local screenshot URL for a template
2532 * @param string $kit_dir Kit directory path
2533 * @param array $template Template data
2534 * @param string $type Type of image (thumbnail or preview)
2535 * @return string|false Local URL or false if not found
2536 */
2537 private function get_local_screenshot_url($kit_dir, $template, $type = 'thumbnail') {
2538 // Check for screenshots folder
2539 $screenshots_dir = $kit_dir . '/screenshots/';
2540 if (!file_exists($screenshots_dir)) {
2541 return false;
2542 }
2543
2544 // Try to find the screenshot file
2545 $template_slug = $template['slug'] ?? $template['id'] ?? '';
2546 if (empty($template_slug)) {
2547 return false;
2548 }
2549
2550 // Common image extensions
2551 $extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
2552
2553 // Possible filename patterns
2554 $patterns = [
2555 $template_slug,
2556 $template_slug . '-' . $type,
2557 $type . '-' . $template_slug,
2558 str_replace('_', '-', $template_slug),
2559 str_replace('-', '_', $template_slug)
2560 ];
2561
2562 foreach ($patterns as $pattern) {
2563 foreach ($extensions as $ext) {
2564 $screenshot_file = $screenshots_dir . $pattern . '.' . $ext;
2565 if (file_exists($screenshot_file)) {
2566 // Convert to URL
2567 $upload_dir = wp_upload_dir();
2568 $relative_path = str_replace($upload_dir['basedir'], '', $screenshot_file);
2569 return $upload_dir['baseurl'] . $relative_path;
2570 }
2571 }
2572 }
2573
2574 // If type is thumbnail, also check without suffix
2575 if ($type === 'thumbnail') {
2576 foreach ($extensions as $ext) {
2577 $screenshot_file = $screenshots_dir . $template_slug . '.' . $ext;
2578 if (file_exists($screenshot_file)) {
2579 // Convert to URL
2580 $upload_dir = wp_upload_dir();
2581 $relative_path = str_replace($upload_dir['basedir'], '', $screenshot_file);
2582 return $upload_dir['baseurl'] . $relative_path;
2583 }
2584 }
2585 }
2586
2587 return false;
2588 }
2589
2590 /**
2591 * Get kit manifest from cache or purchased kits
2592 * @param string $kit_id The kit ID
2593 * @return array|false Manifest data or false
2594 */
2595 public function get_kit_manifest($kit_id) {
2596 // OPTIMIZATION: Use static cache for manifests
2597 static $manifest_cache = [];
2598
2599 if (isset($manifest_cache[$kit_id])) {
2600 return $manifest_cache[$kit_id];
2601 }
2602
2603 // First check if this is a purchased kit
2604 if ($this->is_kit_purchased($kit_id)) {
2605 // Check if manifest is already in purchased kits data
2606 $purchased_kits = $this->get_purchased_kits();
2607 foreach ($purchased_kits as $pk) {
2608 if (($pk['kit_id'] ?? '') === $kit_id && isset($pk['manifest'])) {
2609 $manifest_cache[$kit_id] = $pk['manifest'];
2610 $manifest_cache[$kit_id]['is_purchased'] = true;
2611 return $manifest_cache[$kit_id];
2612 }
2613 }
2614
2615 // First try to read the actual manifest.json file from purchased kit directory
2616 $purchased_kit_dir = $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '/';
2617 $manifest_file = $purchased_kit_dir . 'manifest.json';
2618
2619 if (file_exists($manifest_file)) {
2620 $content = file_get_contents($manifest_file);
2621 $data = json_decode($content, true);
2622 if (json_last_error() === JSON_ERROR_NONE) {
2623 // Add is_purchased flag
2624 $data['is_purchased'] = true;
2625 $manifest_cache[$kit_id] = $data;
2626 return $data;
2627 }
2628 }
2629
2630 // If no manifest file, try to get from stored data
2631 $purchased_kit = $this->get_purchased_kit($kit_id);
2632 if ($purchased_kit) {
2633 // Return manifest if available
2634 if (isset($purchased_kit['manifest'])) {
2635 $purchased_kit['manifest']['is_purchased'] = true;
2636 return $purchased_kit['manifest'];
2637 }
2638
2639 // Check if there's a kit_path with manifest
2640 if (isset($purchased_kit['kit_path'])) {
2641 $manifest_file = $purchased_kit['kit_path'] . '/manifest.json';
2642 if (file_exists($manifest_file)) {
2643 $content = file_get_contents($manifest_file);
2644 $data = json_decode($content, true);
2645 if (json_last_error() === JSON_ERROR_NONE) {
2646 $data['is_purchased'] = true;
2647 return $data;
2648 }
2649 }
2650 }
2651
2652 // Build manifest from purchased kit data
2653 return [
2654 'name' => $purchased_kit['kit_name'] ?? $purchased_kit['title'] ?? '',
2655 'title' => $purchased_kit['kit_name'] ?? $purchased_kit['title'] ?? '',
2656 'description' => $purchased_kit['description'] ?? '',
2657 'author' => $purchased_kit['author'] ?? '',
2658 'pages' => $purchased_kit['templates'] ?? [],
2659 'templates' => $purchased_kit['templates'] ?? [],
2660 'kit_id' => $kit_id,
2661 'is_purchased' => true,
2662 'required_plugins' => $purchased_kit['required_plugins'] ?? [],
2663 'requirements' => $purchased_kit['required_plugins'] ?? [] // Also include as requirements for compatibility
2664 ];
2665 }
2666 }
2667
2668 // Check regular cache
2669 $kit_dir = $this->cache_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2670 $manifest_file = $kit_dir . 'manifest.json';
2671
2672 if (file_exists($manifest_file)) {
2673 $content = file_get_contents($manifest_file);
2674 $data = json_decode($content, true);
2675 return json_last_error() === JSON_ERROR_NONE ? $data : false;
2676 }
2677
2678 return false;
2679 }
2680
2681 /**
2682 * Get kit template JSON from cache or purchased kits
2683 * @param string $kit_id The kit ID
2684 * @param string $template_name The template name/slug
2685 * @return array|false Template data or false
2686 */
2687 public function get_kit_template($kit_id, $template_name) {
2688 // First check if this is a purchased/uploaded kit
2689 if ($this->is_kit_purchased($kit_id)) {
2690 $purchased_kit = $this->get_purchased_kit($kit_id);
2691 if ($purchased_kit && isset($purchased_kit['templates'])) {
2692 foreach ($purchased_kit['templates'] as $template) {
2693 if ($template['slug'] === $template_name || $template['id'] === $template_name) {
2694 return $template;
2695 }
2696 }
2697 }
2698
2699 // If purchased kit has a path, check the file system
2700 if (isset($purchased_kit['kit_path'])) {
2701 $template_file = $purchased_kit['kit_path'] . '/' . sanitize_file_name($template_name) . '.json';
2702 if (file_exists($template_file)) {
2703 $content = file_get_contents($template_file);
2704 $data = json_decode($content, true);
2705 if (json_last_error() === JSON_ERROR_NONE) {
2706 return $data;
2707 }
2708 }
2709 }
2710
2711 // Check purchased kits directory
2712 $purchased_kit_dir = $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2713 $template_file = $purchased_kit_dir . sanitize_file_name($template_name) . '.json';
2714 if (file_exists($template_file)) {
2715 $content = file_get_contents($template_file);
2716 $data = json_decode($content, true);
2717 if (json_last_error() === JSON_ERROR_NONE) {
2718 return $data;
2719 }
2720 }
2721 }
2722
2723 // Check regular cache
2724 $kit_dir = $this->cache_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2725 $template_file = $kit_dir . sanitize_file_name($template_name) . '.json';
2726
2727 if (file_exists($template_file)) {
2728 $content = file_get_contents($template_file);
2729 $data = json_decode($content, true);
2730 return json_last_error() === JSON_ERROR_NONE ? $data : false;
2731 }
2732
2733 return false;
2734 }
2735
2736 /**
2737 * Check if kit is cached or purchased
2738 * @param string $kit_id The kit ID
2739 * @return bool
2740 */
2741 public function is_kit_cached($kit_id) {
2742 // First check if it's a purchased kit
2743 if ($this->is_kit_purchased($kit_id)) {
2744 return true;
2745 }
2746
2747 // Check regular cache
2748 $kit_dir = $this->cache_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2749 $meta_file = $kit_dir . 'meta.json';
2750
2751 return $this->is_cache_valid($meta_file);
2752 }
2753
2754 /**
2755 * Clear kit cache
2756 * @param string $kit_id The kit ID (optional, clears all if not provided)
2757 * @return bool
2758 */
2759 public function clear_kit_cache($kit_id = null) {
2760 if ($kit_id) {
2761 // Clear specific kit
2762 $kit_dir = $this->cache_dir . 'kits/' . sanitize_file_name($kit_id) . '/';
2763 if (file_exists($kit_dir)) {
2764 $this->delete_directory_contents($kit_dir);
2765 return rmdir($kit_dir);
2766 }
2767 } else {
2768 // Clear all kits
2769 $kits_dir = $this->cache_dir . 'kits/';
2770 if (file_exists($kits_dir)) {
2771 $this->delete_directory_contents($kits_dir);
2772 return true;
2773 }
2774 }
2775
2776 return false;
2777 }
2778
2779 /**
2780 * Maybe download kit if not already cached
2781 * @param string $kit_id The kit ID
2782 * @return bool
2783 */
2784 private function maybe_download_kit($kit_id) {
2785 // Check if kit is already cached
2786 if ($this->is_kit_cached($kit_id)) {
2787 return true;
2788 }
2789
2790 // Download the kit in background
2791 $result = $this->download_and_cache_kit($kit_id);
2792 return $result && $result['success'];
2793 }
2794
2795 /**
2796 * Download all available kits
2797 * @return array Download statistics
2798 */
2799 public function download_all_kits() {
2800 $stats = [
2801 'downloaded' => 0,
2802 'failed' => 0,
2803 'skipped' => 0,
2804 'total' => 0
2805 ];
2806
2807 // Get all kits from cache or API
2808 $all_kits = $this->get_cached_kits(false, 'all');
2809
2810 if (!is_array($all_kits)) {
2811 return $stats;
2812 }
2813
2814 // Process each category
2815 foreach ($all_kits as $category => $kits) {
2816 if (!is_array($kits)) {
2817 continue;
2818 }
2819
2820 foreach ($kits as $kit) {
2821 if (!isset($kit['kit_id'])) {
2822 continue;
2823 }
2824
2825 $stats['total']++;
2826
2827 // Check if already cached
2828 if ($this->is_kit_cached($kit['kit_id'])) {
2829 $stats['skipped']++;
2830 continue;
2831 }
2832
2833 // Try to download
2834 $result = $this->download_and_cache_kit($kit['kit_id']);
2835
2836 if ($result && $result['success']) {
2837 $stats['downloaded']++;
2838 } else {
2839 $stats['failed']++;
2840 }
2841
2842 // Add a small delay to avoid overwhelming the server
2843 if ($stats['downloaded'] % 5 === 0) {
2844 sleep(1);
2845 }
2846 }
2847 }
2848
2849 return $stats;
2850 }
2851
2852 /**
2853 * Cache all images from a template kit
2854 */
2855 public function cache_kit_all_images($kit_id, $templates = null) {
2856 if (!$templates) {
2857 // Try to get templates from cache
2858 $cached_data = $this->get_cached_kit_content($kit_id, 'all', false);
2859 if (!$cached_data) {
2860 return false;
2861 }
2862 $templates = $cached_data;
2863 }
2864
2865 $image_count = 0;
2866
2867 // Process each template
2868 foreach ($templates as $template) {
2869 if (isset($template['thumbnail'])) {
2870 $this->cache_image($template['thumbnail'], "kit-{$kit_id}-thumb-{$template['id']}", 'thumbnails');
2871 $image_count++;
2872 }
2873
2874 if (isset($template['content'])) {
2875 // Count images in content
2876 $image_count += $this->count_and_cache_content_images($template['content'], $kit_id);
2877 }
2878 }
2879
2880 return $image_count;
2881 }
2882
2883 /**
2884 * Count and cache images in content
2885 */
2886 private function count_and_cache_content_images($content, $kit_id) {
2887 if (is_string($content)) {
2888 $content = json_decode($content, true);
2889 }
2890
2891 if (!is_array($content)) {
2892 return 0;
2893 }
2894
2895 $count = 0;
2896
2897 // Process the content to find all image URLs
2898 $image_urls = $this->extract_image_urls_from_content($content);
2899
2900 foreach ($image_urls as $url) {
2901 $url_hash = md5($url);
2902 $filename = "kit-{$kit_id}-content-{$url_hash}";
2903 if ($this->cache_image($url, $filename, 'images')) {
2904 $count++;
2905 }
2906 }
2907
2908 return $count;
2909 }
2910
2911 /**
2912 * Extract all image URLs from Elementor content
2913 */
2914 private function extract_image_urls_from_content($content, &$urls = []) {
2915 if (!is_array($content)) {
2916 return $urls;
2917 }
2918
2919 foreach ($content as $element) {
2920 if (isset($element['settings']) && is_array($element['settings'])) {
2921 $this->extract_image_urls_from_settings($element['settings'], $urls);
2922 }
2923
2924 if (isset($element['elements']) && is_array($element['elements'])) {
2925 $this->extract_image_urls_from_content($element['elements'], $urls);
2926 }
2927 }
2928
2929 return array_unique($urls);
2930 }
2931
2932 /**
2933 * Extract image URLs from element settings
2934 */
2935 private function extract_image_urls_from_settings($settings, &$urls) {
2936 if (!is_array($settings)) {
2937 return;
2938 }
2939
2940 foreach ($settings as $key => $value) {
2941 if (is_array($value)) {
2942 // Check if it's an image array with URL
2943 if (isset($value['url']) && filter_var($value['url'], FILTER_VALIDATE_URL)) {
2944 $urls[] = $value['url'];
2945 }
2946 // Recursively check nested arrays
2947 $this->extract_image_urls_from_settings($value, $urls);
2948 } elseif (is_string($value)) {
2949 // Check if the string contains image URLs
2950 if (preg_match_all('/(https?:\/\/[^\s"]+\.(?:jpg|jpeg|png|gif|svg|webp))/i', $value, $matches)) {
2951 $urls = array_merge($urls, $matches[1]);
2952 }
2953 }
2954 }
2955 }
2956
2957 /**
2958 * Store a purchased template kit permanently
2959 * @param array $kit_data The kit data to store
2960 * @return bool Success status
2961 */
2962 public function store_purchased_kit($kit_data) {
2963 if (!file_exists($this->purchased_dir)) {
2964 wp_mkdir_p($this->purchased_dir);
2965
2966 // Create subdirectories
2967 $subdirs = ['kits', 'manifests', 'thumbnails', 'metadata'];
2968 foreach ($subdirs as $subdir) {
2969 $subdir_path = $this->purchased_dir . $subdir . '/';
2970 if (!file_exists($subdir_path)) {
2971 wp_mkdir_p($subdir_path);
2972 }
2973 }
2974 }
2975
2976 // Extract kit ID and normalize it
2977 $kit_id = $kit_data['kit_id'] ?? $kit_data['template_id'] ?? '';
2978 if (empty($kit_id)) {
2979 return false;
2980 }
2981
2982 // If kit_path is not provided but we have a kit_id, check if it exists in purchased_kits
2983 if (!isset($kit_data['kit_path']) && !empty($kit_id)) {
2984 $potential_path = $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '/';
2985 if (file_exists($potential_path)) {
2986 $kit_data['kit_path'] = $potential_path;
2987 }
2988 }
2989
2990 // Process manifest and templates to update URLs if kit_path is provided
2991 if (isset($kit_data['kit_path']) && file_exists($kit_data['kit_path'])) {
2992 $this->process_extracted_kit_manifest($kit_data['kit_path']);
2993 $this->process_template_json_files($kit_data['kit_path']);
2994 $this->process_nav_menu_json($kit_data['kit_path']);
2995
2996 // Re-read the manifest after processing
2997 $manifest_file = $kit_data['kit_path'] . '/manifest.json';
2998 if (file_exists($manifest_file)) {
2999 $content = file_get_contents($manifest_file);
3000 $manifest = json_decode($content, true);
3001 if ($manifest) {
3002 $kit_data['manifest'] = $manifest;
3003
3004 // Update templates from processed manifest
3005 if (isset($manifest['templates'])) {
3006 $kit_data['templates'] = $manifest['templates'];
3007 } elseif (isset($manifest['pages'])) {
3008 $kit_data['templates'] = $manifest['pages'];
3009 }
3010 }
3011 }
3012 }
3013
3014 // IMPORTANT: Process URLs FIRST before creating metadata to ensure NO remote URLs are saved
3015 $processed_kit_data = $this->process_kit_data_urls($kit_data, $kit_id);
3016
3017 // Store kit metadata with same structure as cached kits - using PROCESSED data
3018 $metadata = [
3019 'kit_id' => $kit_id,
3020 'kit_name' => $processed_kit_data['kit_name'] ?? $processed_kit_data['title'] ?? '',
3021 'purchased_date' => current_time('mysql'),
3022 'is_purchased' => true,
3023 'purchasable' => false,
3024 'downloadable' => true,
3025 'is_pro' => false, // No restrictions for purchased templates
3026 'categories' => $processed_kit_data['categories'] ?? ['purchased'],
3027 'keywords' => $processed_kit_data['keywords'] ?? [],
3028 'thumbnail' => $processed_kit_data['thumbnail'] ?? '', // FIXED: Use processed thumbnail (local URL)
3029 'preview_url' => $processed_kit_data['preview_url'] ?? '', // FIXED: Use processed preview URL
3030 'descriptions' => $processed_kit_data['descriptions'] ?? $processed_kit_data['description'] ?? '',
3031 'downloads' => $processed_kit_data['downloads'] ?? 0,
3032 'purchase_url' => '', // Empty since already purchased
3033 'template_count' => isset($processed_kit_data['templates']) ? count($processed_kit_data['templates']) : 1,
3034 'required_plugins' => $processed_kit_data['required_plugins'] ?? [], // Store required plugins
3035 'kit_path' => $processed_kit_data['kit_path'] ?? null // Store the path if available
3036 ];
3037
3038 // Update metadata with processed manifest data if available
3039 if (isset($processed_kit_data['manifest'])) {
3040 // Preserve the full processed manifest (with local URLs)
3041 $metadata['manifest'] = $processed_kit_data['manifest'];
3042
3043 // Also extract required_plugins at the top level for easy access
3044 if (isset($processed_kit_data['manifest']['required_plugins'])) {
3045 $metadata['required_plugins'] = $processed_kit_data['manifest']['required_plugins'];
3046 }
3047 }
3048
3049 // Save metadata with processed URLs
3050 $metadata_file = $this->purchased_dir . 'metadata/' . sanitize_file_name($kit_id) . '.json';
3051 $this->write_cache_file($metadata_file, $metadata);
3052
3053 // Save full kit data if provided (already processed)
3054 if (isset($processed_kit_data['content']) || isset($processed_kit_data['templates']) || isset($processed_kit_data['manifest'])) {
3055 $kit_file = $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '.json';
3056 $this->write_cache_file($kit_file, $processed_kit_data);
3057 }
3058
3059 return true;
3060 }
3061
3062 /**
3063 * Update existing purchased kits to remove remote URLs and use local URLs
3064 * @return int Number of kits updated
3065 */
3066 public function update_existing_purchased_kits_urls() {
3067 if (!file_exists($this->purchased_dir . 'metadata/')) {
3068 return 0;
3069 }
3070
3071 $updated_count = 0;
3072 $metadata_files = glob($this->purchased_dir . 'metadata/*.json');
3073
3074 foreach ($metadata_files as $metadata_file) {
3075 $metadata = $this->read_cache_file($metadata_file);
3076 if (!$metadata) {
3077 continue;
3078 }
3079
3080 $kit_id = $metadata['kit_id'] ?? '';
3081 if (empty($kit_id)) {
3082 continue;
3083 }
3084
3085 $has_remote_urls = false;
3086
3087 // Check if metadata has remote URLs
3088 if (isset($metadata['thumbnail']) && strpos($metadata['thumbnail'], 'http') === 0) {
3089 $has_remote_urls = true;
3090 }
3091 if (isset($metadata['preview_url']) && strpos($metadata['preview_url'], 'http') === 0) {
3092 $has_remote_urls = true;
3093 }
3094 if (isset($metadata['manifest']['thumbnail']) && strpos($metadata['manifest']['thumbnail'], 'http') === 0) {
3095 $has_remote_urls = true;
3096 }
3097 if (isset($metadata['manifest']['thumbnail_url']) && strpos($metadata['manifest']['thumbnail_url'], 'http') === 0) {
3098 $has_remote_urls = true;
3099 }
3100
3101 if ($has_remote_urls) {
3102 // Process the metadata to convert remote URLs to local
3103 $processed_metadata = $this->process_kit_data_urls($metadata, $kit_id);
3104
3105 // Save the updated metadata
3106 $this->write_cache_file($metadata_file, $processed_metadata);
3107
3108 // Also update the kit data file if it exists
3109 $kit_file = $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '.json';
3110 if (file_exists($kit_file)) {
3111 $kit_data = $this->read_cache_file($kit_file);
3112 if ($kit_data) {
3113 $processed_kit_data = $this->process_kit_data_urls($kit_data, $kit_id);
3114 $this->write_cache_file($kit_file, $processed_kit_data);
3115 }
3116 }
3117
3118 $updated_count++;
3119 }
3120 }
3121
3122 return $updated_count;
3123 }
3124
3125 /**
3126 * Get all purchased kits
3127 * @return array Array of purchased kits
3128 */
3129 public function get_purchased_kits() {
3130 static $cached_purchased_kits = null;
3131
3132 if ($cached_purchased_kits !== null) {
3133 return $cached_purchased_kits;
3134 }
3135
3136 if (!file_exists($this->purchased_dir . 'metadata/')) {
3137 $cached_purchased_kits = [];
3138 return [];
3139 }
3140
3141 $purchased_kits = [];
3142 $metadata_files = glob($this->purchased_dir . 'metadata/*.json');
3143
3144 // OPTIMIZATION: Batch read all metadata files
3145 foreach ($metadata_files as $file) {
3146 $metadata = $this->read_cache_file($file);
3147 if ($metadata) {
3148 // Pre-load manifest data if available in the same directory
3149 $kit_id = $metadata['kit_id'] ?? '';
3150 if ($kit_id) {
3151 // Check if manifest.json exists in the kit directory
3152 $manifest_file = $this->purchased_dir . 'kits/kit_' . $kit_id . '/manifest.json';
3153 if (file_exists($manifest_file) && !isset($metadata['manifest'])) {
3154 $manifest_content = @file_get_contents($manifest_file);
3155 if ($manifest_content) {
3156 $manifest_data = json_decode($manifest_content, true);
3157 if ($manifest_data) {
3158 $metadata['manifest'] = $manifest_data;
3159 }
3160 }
3161 }
3162 }
3163 $purchased_kits[] = $metadata;
3164 }
3165 }
3166
3167 $cached_purchased_kits = $purchased_kits;
3168 return $purchased_kits;
3169 }
3170
3171 /**
3172 * Check if a kit is purchased
3173 * @param string $kit_id The kit ID to check
3174 * @return bool
3175 */
3176 public function is_kit_purchased($kit_id) {
3177 // Check with exact kit_id
3178 $metadata_file = $this->purchased_dir . 'metadata/' . sanitize_file_name($kit_id) . '.json';
3179 if (file_exists($metadata_file)) {
3180 return true;
3181 }
3182
3183 // Also check with kit_ prefix format for compatibility
3184 $metadata_file_alt = $this->purchased_dir . 'metadata/kit_' . sanitize_file_name($kit_id) . '.json';
3185 if (file_exists($metadata_file_alt)) {
3186 return true;
3187 }
3188
3189 // Check if the kit folder exists directly
3190 $kit_dir = $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '/';
3191 return file_exists($kit_dir);
3192 }
3193
3194 /**
3195 * Get purchased kit data
3196 * @param string $kit_id The kit ID
3197 * @return array|false Kit data or false if not found
3198 */
3199 public function get_purchased_kit($kit_id) {
3200 // Try direct metadata file first
3201 $metadata_file = $this->purchased_dir . 'metadata/' . sanitize_file_name($kit_id) . '.json';
3202 if (file_exists($metadata_file)) {
3203 return $this->read_cache_file($metadata_file);
3204 }
3205
3206 // Try with kit_ prefix
3207 $metadata_file_alt = $this->purchased_dir . 'metadata/kit_' . sanitize_file_name($kit_id) . '.json';
3208 if (file_exists($metadata_file_alt)) {
3209 return $this->read_cache_file($metadata_file_alt);
3210 }
3211
3212 // Try kits directory with stored data
3213 $kit_file = $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '.json';
3214 if (file_exists($kit_file)) {
3215 return $this->read_cache_file($kit_file);
3216 }
3217
3218 // Try kits directory with kit_ prefix
3219 $kit_file_alt = $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '.json';
3220 if (file_exists($kit_file_alt)) {
3221 return $this->read_cache_file($kit_file_alt);
3222 }
3223
3224 return false;
3225 }
3226
3227 /**
3228 * Delete a purchased kit (if needed for management)
3229 * @param string $kit_id The kit ID to delete
3230 * @return bool
3231 */
3232 public function delete_purchased_kit($kit_id) {
3233 $deleted = false;
3234
3235 // Delete metadata files (try both with and without kit_ prefix)
3236 $metadata_files = [
3237 $this->purchased_dir . 'metadata/' . sanitize_file_name($kit_id) . '.json',
3238 $this->purchased_dir . 'metadata/kit_' . sanitize_file_name($kit_id) . '.json'
3239 ];
3240
3241 foreach ($metadata_files as $metadata_file) {
3242 if (file_exists($metadata_file)) {
3243 wp_delete_file($metadata_file);
3244 $deleted = true;
3245 }
3246 }
3247
3248 // Delete kit data files
3249 $kit_files = [
3250 $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id) . '.json',
3251 $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id) . '.json'
3252 ];
3253
3254 foreach ($kit_files as $kit_file) {
3255 if (file_exists($kit_file)) {
3256 wp_delete_file($kit_file);
3257 $deleted = true;
3258 }
3259 }
3260
3261 // Delete kit directory and all its contents
3262 $kit_directories = [
3263 $this->purchased_dir . 'kits/kit_' . sanitize_file_name($kit_id),
3264 $this->purchased_dir . 'kits/' . sanitize_file_name($kit_id)
3265 ];
3266
3267 foreach ($kit_directories as $kit_dir) {
3268 if (file_exists($kit_dir) && is_dir($kit_dir)) {
3269 // Delete all contents recursively
3270 $this->delete_directory_contents($kit_dir);
3271 @rmdir($kit_dir);
3272 $deleted = true;
3273 }
3274 }
3275
3276 // Clear any transient cache related to this kit
3277 delete_transient('jltma_kit_' . $kit_id);
3278 delete_transient('jltma_kit_templates_' . $kit_id);
3279 delete_transient('jltma_kit_manifest_' . $kit_id);
3280
3281 return $deleted;
3282 }
3283
3284 /**
3285 * Get singleton instance
3286 */
3287 public static function get_instance()
3288 {
3289 if (self::$instance === null) {
3290 self::$instance = new self();
3291 }
3292 return self::$instance;
3293 }
3294 }
3295
3296 // Initialize template kit cache manager
3297 Template_Kit_Cache::get_instance();