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

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

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