PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.5.1
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.5.1
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
templately / includes / Core / Importer / Utils / AIUtils.php

AIUtils.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.5.1, at includes/Core/Importer/Utils/AIUtils.php

961 lines 30.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Core\Importer\Utils;
4
5 use Templately\Utils\Options;
6 use Templately\Utils\Helper;
7
8 /**
9 * AI-related utility functions for Templately
10 * Handles AI process data management using API key-based storage with count-based cleanup
11 */
12 class AIUtils {
13
14 /**
15 * Sanitize path component for security - prevents path traversal attacks
16 *
17 * @param mixed $value The value to sanitize (string or numeric)
18 * @param string $type Type for error message ('session_id', 'content_id', 'path_key')
19 * @return string|\WP_Error Sanitized value or error if invalid
20 */
21 public static function sanitize_path_component($value, $type = 'path_component') {
22 // Convert numeric values to string (content IDs like 136 are valid)
23 if (is_numeric($value)) {
24 $value = (string) $value;
25 }
26
27 // Check for empty or non-string values
28 if (empty($value) || !is_string($value)) {
29 return Helper::error(
30 'invalid_' . $type,
31 sprintf(__('Invalid %s: empty or not a valid value.', 'templately'), $type),
32 'sanitize_path_component',
33 400
34 );
35 }
36
37 // Check for path traversal attempts BEFORE sanitizing
38 if (strpos($value, '..') !== false ||
39 strpos($value, '/') !== false ||
40 strpos($value, '\\') !== false) {
41 return Helper::error(
42 'invalid_' . $type,
43 sprintf(__('Invalid %s: contains path separators.', 'templately'), $type),
44 'sanitize_path_component',
45 400
46 );
47 }
48
49 // Apply WordPress sanitize_file_name for additional safety
50 return sanitize_file_name($value);
51 }
52
53 /**
54 * Validate that a file path is within WordPress upload directory
55 * Prevents path traversal attacks
56 *
57 * @param string $file_path The file path to validate
58 * @return bool|WP_Error True if valid, WP_Error otherwise
59 */
60 public static function validate_file_path($file_path) {
61 // Get WordPress upload directory
62 $upload_dir = wp_upload_dir();
63 $real_upload_base = realpath($upload_dir['basedir']);
64
65 if ($real_upload_base === false) {
66 return Helper::error(
67 'invalid_upload_dir',
68 __('WordPress upload directory is not accessible.', 'templately'),
69 'validate_file_path',
70 500
71 );
72 }
73
74 // For file path, check the directory if file doesn't exist yet
75 $dir_to_check = dirname($file_path);
76
77 // Create directory if it doesn't exist (for pre-write validation)
78 if (!file_exists($dir_to_check)) {
79 wp_mkdir_p($dir_to_check);
80 }
81
82 $real_path = realpath($dir_to_check);
83 if ($real_path === false) {
84 return Helper::error(
85 'path_not_exists',
86 __('File path does not exist or is not accessible.', 'templately'),
87 'validate_file_path',
88 400
89 );
90 }
91
92 // Normalize paths with trailing directory separator
93 $normalized_upload_base = rtrim($real_upload_base, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
94 $normalized_path = rtrim($real_path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
95
96 // Check if path starts with upload directory (PHP 8+ style with polyfill)
97 // This prevents partial matches like /var/www/uploads-backup matching /var/www/uploads
98 $is_subdirectory = false;
99 if (function_exists('str_starts_with')) {
100 // PHP 8+
101 $is_subdirectory = str_starts_with($normalized_path, $normalized_upload_base);
102 } else {
103 // PHP 7.4 polyfill
104 $is_subdirectory = substr($normalized_path, 0, strlen($normalized_upload_base)) === $normalized_upload_base;
105 }
106
107 if (!$is_subdirectory) {
108 return Helper::error(
109 'path_outside_uploads',
110 __('Invalid file path: path is outside WordPress upload directory.', 'templately'),
111 'validate_file_path',
112 400
113 );
114 }
115
116 return true;
117 }
118
119 /**
120 * Handle SSE message wait with timeout exit condition
121 * Static utility function for reusable timeout handling across different import contexts
122 *
123 * @param string $session_id Session ID for progress tracking
124 * @param string $progress_id Progress tracking identifier (e.g., 'ai_content_time', 'finalize_time')
125 * @param array $updated_ids Currently updated/processed pages
126 * @param array $ai_page_ids All AI pages that need processing
127 * @param callable $sse_message_callback Callback function for sending SSE messages
128 * @param array $additional_sse_data Additional data to include in SSE message
129 * @param string|null $old_template_id Optional template ID for local site polling
130 * @return bool True if should continue processing, false if should exit
131 */
132 public static function handle_sse_wait_with_timeout($session_id, $progress_id, $updated_ids, $ai_page_ids, $sse_message_callback, $additional_sse_data = [], $old_template_id = null, $timeout_seconds = 420) {
133 $total_pages = count($ai_page_ids);
134 $updated_pages = count($updated_ids['pages'] ?? []);
135
136 // If all pages are processed or credit cost is available, continue processing
137 if ($total_pages <= $updated_pages || isset($updated_ids['credit_cost'])) {
138 return true;
139 }
140
141 // Get timeout tracking data from session
142 $session_data = SessionData::get_data($session_id);
143 $progress_data = $session_data['progress'][$progress_id] ?? [];
144 $last_progress = $progress_data['last_progress'] ?? 0;
145 $last_time = $progress_data['last_time'] ?? 0;
146 $current_time = time();
147 $progress_percentage = $total_pages > 0 ? round(($updated_pages / $total_pages) * 100) : 0;
148 $is_local_site = $session_data['isLocalSite'] ?? false;
149
150 // Skip timeout check if credit cost is available and time difference is > 10 seconds
151 if (isset($updated_ids['credit_cost']) && !empty($last_time) && ($current_time - $last_time) > 10) {
152 return true;
153 }
154
155 // Check if time difference is less than timeout(default 7 minutes) (timeout condition)
156 if (empty($last_time) || ($current_time - $last_time) < $timeout_seconds) {
157 // For local sites with template ID, attempt polling before waiting
158 if ($is_local_site && !empty($old_template_id)) {
159 $process_id = $session_data['process_id'] ?? null;
160 if (!empty($process_id)) {
161 // Call generic polling function to process all available templates
162 $polling_result = self::poll_for_template($process_id, $session_id, $ai_page_ids);
163 if ($polling_result === true) {
164 // Check if the specific template file now exists after polling
165 $upload_dir = wp_upload_dir();
166 $tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . $session_id . DIRECTORY_SEPARATOR;
167
168 // Find the correct directory for the template ID
169 $template_file_found = false;
170 foreach ($ai_page_ids as $key => $ids) {
171 if (in_array($old_template_id, $ids)) {
172 $page_dir = $tmp_dir . $key . DIRECTORY_SEPARATOR;
173 $file_path = $page_dir . $old_template_id . '.ai.json';
174 if (file_exists($file_path)) {
175 $template_file_found = true;
176 break;
177 }
178 }
179 }
180
181 if ($template_file_found) {
182 // Specific template found and downloaded, continue processing
183 return true;
184 }
185 }
186 }
187 }
188
189 // Only update time if progress has changed
190 if ($progress_percentage !== $last_progress) {
191 $updated_progress = $session_data['progress'] ?? [];
192 $updated_progress[$progress_id] = [
193 'last_progress' => $progress_percentage,
194 'last_time' => $current_time,
195 ];
196 SessionData::set($session_id, 'progress', $updated_progress);
197 }
198
199 // Prepare SSE message data
200 $sse_data = array_merge([
201 'type' => 'wait',
202 'action' => 'wait',
203 'generated_pages' => $updated_ids,
204 'all_pages' => $ai_page_ids,
205 ], $additional_sse_data);
206
207 // Send wait message and exit
208 call_user_func($sse_message_callback, $sse_data);
209 exit;
210 }
211
212 // Timeout exceeded - could send error message here if needed
213 // Currently commented out in original AIContent.php
214 /*
215 call_user_func($sse_message_callback, [
216 'action' => 'error',
217 'status' => 'error',
218 'type' => "error",
219 'title' => __("Oops!", "templately"),
220 'message' => __("Taking too long....", "templately"),
221 ]);
222 exit;
223 */
224
225 // Continue processing if timeout exceeded (current behavior)
226 return true;
227 }
228
229 /**
230 * Poll for logo generation status on local sites
231 * Makes GET request to API endpoint and returns logo generation data
232 *
233 * @param string $process_id The logo generation process ID
234 * @return array|WP_Error Logo generation data (images, credit_cost) or error
235 */
236 public static function poll_for_logo_generation($process_id) {
237 if (empty($process_id)) {
238 return Helper::error(
239 'invalid_process_id',
240 __('Process ID is required for logo polling.', 'templately'),
241 'poll_for_logo_generation',
242 400
243 );
244 }
245
246 // Make GET request to API endpoint
247 $response = Helper::make_api_get_request("v2/get-generated-logo/{$process_id}", [], [], 30);
248
249 if (is_wp_error($response)) {
250 return $response;
251 }
252
253 $response_code = wp_remote_retrieve_response_code($response);
254 if ($response_code !== 200) {
255 return Helper::error(
256 'api_error',
257 sprintf(__('API returned HTTP %d error.', 'templately'), $response_code),
258 'poll_for_logo_generation',
259 $response_code
260 );
261 }
262
263 $body = wp_remote_retrieve_body($response);
264 $api_data = json_decode($body, true);
265
266 if (!isset($api_data['status']) || $api_data['status'] !== 'success') {
267 return Helper::error(
268 'api_response_error',
269 __('API returned an error response.', 'templately'),
270 'poll_for_logo_generation',
271 400
272 );
273 }
274
275 $response_data = $api_data['data'] ?? [];
276
277 // Return the logo generation data (images array, credit_cost, etc.)
278 return [
279 'status' => 'success',
280 'data' => $response_data,
281 ];
282 }
283
284 /**
285 * Poll for AI template generation status on local sites
286 * Generic polling utility that processes all available templates
287 *
288 * @param string $process_id The AI process ID
289 * @param string $session_id The session ID
290 * @param array $ai_page_ids The AI page IDs structure
291 * @return bool True if polling was successful, false otherwise
292 */
293 public static function poll_for_template($process_id, $session_id, $ai_page_ids) {
294 // First check if polling is already complete
295 $processed_pages = get_option("templately_ai_processed_pages", []);
296 if (isset($processed_pages[$process_id]['is_last_part']) && $processed_pages[$process_id]['is_last_part']) {
297 return true;
298 }
299
300 // Make GET request to API endpoint
301 $response = Helper::make_api_get_request("v2/ai/{$process_id}/template", [], [], 30);
302
303 if (is_wp_error($response)) {
304 return false;
305 }
306
307 $response_code = wp_remote_retrieve_response_code($response);
308 if ($response_code !== 200) {
309 return false;
310 }
311
312 $body = wp_remote_retrieve_body($response);
313 $api_data = json_decode($body, true);
314
315 if (!isset($api_data['status']) || $api_data['status'] !== 'success') {
316 return false;
317 }
318
319 $response_data = $api_data['data'] ?? [];
320
321 // Extract required fields from API response
322 $is_last_part = $response_data['is_last_part'] ?? false;
323 $credit_cost = $response_data['credit_cost'] ?? 0;
324 $templates = $response_data['templates'] ?? [];
325
326 // Save credit_cost if available
327 if ($credit_cost > 0) {
328 $processed_pages = get_option("templately_ai_processed_pages", []);
329 $processed_pages[$process_id] = $processed_pages[$process_id] ?? [];
330 $processed_pages[$process_id]['credit_cost'] = $credit_cost;
331 update_option("templately_ai_processed_pages", $processed_pages, false);
332 }
333
334 // Save is_last_part if true
335 if ($is_last_part) {
336 $processed_pages = get_option("templately_ai_processed_pages", []);
337 $processed_pages[$process_id] = $processed_pages[$process_id] ?? [];
338 $processed_pages[$process_id]['is_last_part'] = $is_last_part;
339 update_option("templately_ai_processed_pages", $processed_pages, false);
340 }
341
342 // Process ALL templates if available (extracted from FullSiteImport::ai_poll_template)
343 if (!empty($templates) && is_array($templates)) {
344 foreach ($templates as $content_id => $template_data) {
345 // Save template to file using the common helper function
346 $result = self::save_template_to_file(
347 $process_id,
348 $session_id,
349 $content_id,
350 $template_data,
351 $ai_page_ids,
352 false // Not skipped
353 );
354 // Continue processing other templates even if one fails
355 }
356 }
357
358 return true; // Return true if polling was successful, regardless of specific templates
359 }
360
361 /**
362 * Get AI process data from WordPress options
363 *
364 * @return array The AI process data array
365 */
366 public static function get_ai_process_data() {
367 $all_ai_process_data = get_option('templately_ai_process_data', []);
368 return is_array($all_ai_process_data) ? $all_ai_process_data : [];
369 }
370
371 /**
372 * Update AI process data in WordPress options with process_id as key
373 * Appends to existing data
374 *
375 * @param array $data The AI process data to save (process_id => process_data)
376 * @return bool True on success, false on failure
377 */
378 public static function update_ai_process_data($data) {
379 if (!is_array($data)) {
380 return false;
381 }
382
383 // Get existing data
384 $all_ai_process_data = get_option('templately_ai_process_data', []);
385 if (!is_array($all_ai_process_data)) {
386 $all_ai_process_data = [];
387 }
388
389 // Append new data to existing data
390 foreach ($data as $process_id => $process_data) {
391 if (is_array($process_data)) {
392 $all_ai_process_data[$process_id] = $process_data;
393 }
394 }
395
396 return update_option('templately_ai_process_data', $all_ai_process_data);
397 }
398
399 /**
400 * Get the latest AI process data for the current API key or user ID
401 * Used in import_info() to return the most recent AI process
402 * Priority: api_key first, then user_id as fallback
403 *
404 * @return array|null The latest AI process data or null if not found
405 */
406 public static function get_latest_ai_process_by_api_key($id) {
407 $api_key = Options::get_instance()->get('api_key');
408 $user = Options::get_instance()->get('user');
409 $user_id = isset($user['id']) ? $user['id'] : null;
410
411 $all_ai_process_data = get_option('templately_ai_process_data', []);
412 if (!is_array($all_ai_process_data)) {
413 return null;
414 }
415
416 $matching_processes = [];
417
418 // Priority 1: Try to find processes by API key if available
419 if (!empty($api_key)) {
420 foreach ($all_ai_process_data as $process_id => $process_data) {
421 if (is_array($process_data) && isset($process_data['api_key']) && $process_data['api_key'] === $api_key) {
422 $matching_processes[$process_id] = $process_data;
423 }
424 }
425 }
426
427 // Priority 2: If no API key matches found or API key is empty, fallback to user_id
428 if (empty($matching_processes) && !empty($user_id)) {
429 foreach ($all_ai_process_data as $process_id => $process_data) {
430 if (is_array($process_data) && isset($process_data['user_id']) && $process_data['user_id'] === $user_id) {
431 $matching_processes[$process_id] = $process_data;
432 }
433 }
434 }
435
436 if (empty($matching_processes)) {
437 return null;
438 }
439
440 $latest_data = end($matching_processes);
441
442 if($id != $latest_data['pack_id'] && isset($latest_data['imageReplace'])){
443 unset($latest_data['imageReplace']);
444 }
445
446 // Return the last (most recent) process
447 return $latest_data;
448 }
449
450
451
452 /**
453 * Get AI process data by session ID
454 *
455 * @param string $session_id The session ID to search for
456 * @return array|null The AI process data array or null if not found
457 */
458 public static function get_ai_process_data_by_session_id($session_id) {
459 if (empty($session_id)) {
460 return null;
461 }
462
463 $ai_process_data = self::get_ai_process_data();
464
465 foreach ($ai_process_data as $process) {
466 if (is_array($process) && isset($process['session_id']) && $process['session_id'] === $session_id) {
467 return $process;
468 }
469 }
470
471 return null;
472 }
473
474 /**
475 * Get AI process ID by session ID
476 *
477 * @param string $session_id The session ID to search for
478 * @return string|null The process ID (array key) or null if not found
479 */
480 public static function get_ai_process_id_by_session_id($session_id) {
481 if (empty($session_id)) {
482 return null;
483 }
484
485 $ai_process_data = self::get_ai_process_data();
486
487 foreach ($ai_process_data as $process_id => $process) {
488 if (is_array($process) && isset($process['session_id']) && $process['session_id'] === $session_id) {
489 return $process_id;
490 }
491 }
492
493 return null;
494 }
495
496 /**
497 * Get AI process data by process ID
498 *
499 * @param string $process_id The process ID to search for
500 * @return array|null The AI process data array or null if not found
501 */
502 public static function get_ai_process_data_by_process_id($process_id) {
503 if (empty($process_id)) {
504 return null;
505 }
506
507 $ai_process_data = self::get_ai_process_data();
508
509 if (isset($ai_process_data[$process_id]) && is_array($ai_process_data[$process_id])) {
510 return $ai_process_data[$process_id];
511 }
512
513 return null;
514 }
515
516 /**
517 * Clean AI process data by pack ID, keeping only the current process
518 * Removes all AI process entries with the same pack_id except the current process
519 *
520 * @param string $pack_id The pack ID to match for cleanup
521 * @param string $current_process_id The current process ID to preserve (optional)
522 * @return array Array of removed process IDs
523 */
524 public static function clean_ai_process_data_by_pack_id($pack_id, $current_process_id = null) {
525 if (empty($pack_id)) {
526 return [];
527 }
528
529 $all_ai_process_data = get_option('templately_ai_process_data', []);
530 if (!is_array($all_ai_process_data)) {
531 return [];
532 }
533
534 $removed_process_ids = [];
535
536 foreach ($all_ai_process_data as $process_id => $process_data) {
537 if (!is_array($process_data)) {
538 continue;
539 }
540
541 // Skip the current process by process_id if provided
542 if (!empty($current_process_id) && $process_id === $current_process_id) {
543 continue;
544 }
545
546 // Remove processes that have the same pack_id
547 if (isset($process_data['pack_id']) && $process_data['pack_id'] === $pack_id) {
548 unset($all_ai_process_data[$process_id]);
549 $removed_process_ids[] = $process_id;
550 }
551 }
552
553 // Update the options with cleaned data if any processes were removed
554 if (!empty($removed_process_ids)) {
555 update_option('templately_ai_process_data', $all_ai_process_data);
556 }
557
558 return $removed_process_ids;
559 }
560
561 /**
562 * Save template data to file
563 *
564 * Common function for saving templates to files, used by AI content operations
565 *
566 * @param string $process_id The process ID
567 * @param string $session_id The session ID
568 * @param string $content_id The content ID
569 * @param string $template The template data (base64 encoded or raw)
570 * @param array $ai_page_ids Array of AI page IDs
571 * @param bool $is_skipped Whether the template was skipped
572 * @return array|WP_Error Result array with status and data
573 */
574 public static function save_template_to_file($process_id, $session_id, $content_id, $template, $ai_page_ids, $is_skipped = false) {
575 // Security: Sanitize session_id
576 $session_id = self::sanitize_path_component($session_id, 'session_id');
577 if (is_wp_error($session_id)) {
578 return $session_id;
579 }
580
581 // Security: Sanitize content_id
582 $content_id = self::sanitize_path_component($content_id, 'content_id');
583 if (is_wp_error($content_id)) {
584 return $content_id;
585 }
586
587 $upload_dir = wp_upload_dir();
588
589 // Always save to tmp directory for AI content workflow
590 $tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . $session_id . DIRECTORY_SEPARATOR;
591
592 // Decode template if it's base64 encoded
593 if (! empty($template) && base64_decode($template, true) !== false) {
594 $template = base64_decode($template);
595 }
596
597 // Handle empty template (skipped)
598 if (empty($template)) {
599 $template = json_encode([
600 "isSkipped" => true,
601 ]);
602 }
603
604 // Find the correct directory for the content ID
605 $found_key = null;
606 foreach ($ai_page_ids as $key => $ids) {
607 if (in_array($content_id, $ids)) {
608 $found_key = $key;
609 break;
610 }
611 }
612
613 if ($found_key === null) {
614 return Helper::error('invalid_content_id', __('Content ID not found in AI page IDs.', 'templately'), 'save_template_to_file', 400);
615 }
616
617 // Security: Sanitize found_key parts (e.g., "templates/page" -> sanitize each part)
618 $key_parts = explode('/', $found_key);
619 $sanitized_parts = [];
620 foreach ($key_parts as $part) {
621 $sanitized = self::sanitize_path_component($part, 'path_key');
622 if (is_wp_error($sanitized)) {
623 return $sanitized;
624 }
625 $sanitized_parts[] = $sanitized;
626 }
627 $found_key = implode(DIRECTORY_SEPARATOR, $sanitized_parts);
628
629 // Create directory and file path
630 $page_dir = $tmp_dir . $found_key . DIRECTORY_SEPARATOR;
631 $file_path = $page_dir . $content_id . '.ai.json';
632
633 // Security: Validate path is within expected directory before writing
634 $validation = self::validate_file_path($file_path);
635 if (is_wp_error($validation)) {
636 return $validation;
637 }
638
639 wp_mkdir_p($page_dir);
640
641 // Save the file
642 $is_success = file_put_contents($file_path, $template);
643
644 if ($is_success) {
645 // Update processed pages option
646 $processed_pages = get_option("templately_ai_processed_pages", []);
647 $processed_pages[$process_id] = $processed_pages[$process_id] ?? [];
648 $processed_pages[$process_id]['pages'][$content_id] = $is_skipped;
649 update_option("templately_ai_processed_pages", $processed_pages, false);
650
651 return [
652 'status' => 'success',
653 'data' => [
654 'process_id' => $process_id,
655 // 'file_path' => $file_path,
656 'content_id' => $content_id,
657 ],
658 ];
659 }
660
661 return Helper::error('file_save_failed', __('Failed to save template file.', 'templately'), 'save_template_to_file', 500);
662 }
663
664 /**
665 * Get matched session data by process ID
666 *
667 * Helper function to retrieve session data for a given process ID
668 * Optimized to use AI process data which already contains session_id,
669 * avoiding the need to load all session data.
670 *
671 * @param string $process_id The process ID to match
672 * @return array|false Returns matched data array or false if not found
673 */
674 public static function get_matched_session_data($process_id) {
675 if (empty($process_id)) {
676 return false;
677 }
678
679 // Get AI process data which contains the session_id
680 $process_data = self::get_ai_process_data_by_process_id($process_id);
681
682 if (!empty($process_data['session_id'])) {
683 // Direct lookup using session_id - no loading all sessions
684 return SessionData::get_data($process_data['session_id']);
685 }
686
687 return false;
688 }
689
690 /**
691 * Generate AI file paths for content processing
692 *
693 * @param string $session_id The session ID
694 * @param string $type The content type (templates, content, etc.)
695 * @param string $sub_type The content sub-type (page, post, etc.)
696 * @param string $template_id The template ID
697 * @param string $dir_path The base directory path for original files
698 * @return array Array containing paths for original and AI files
699 */
700 public static function generate_ai_file_paths($session_id, $type, $sub_type, $template_id, $dir_path) {
701 // Original file path
702 $original_path = $dir_path . $type . DIRECTORY_SEPARATOR;
703 if (!empty($sub_type)) {
704 $original_path .= $sub_type . DIRECTORY_SEPARATOR;
705 }
706 $original_file = $original_path . "{$template_id}.json";
707
708 // AI file path in tmp directory
709 $upload_dir = wp_upload_dir();
710 $tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . $session_id . DIRECTORY_SEPARATOR;
711 $ai_path = $tmp_dir . $type . DIRECTORY_SEPARATOR;
712 if (!empty($sub_type)) {
713 $ai_path .= $sub_type . DIRECTORY_SEPARATOR;
714 }
715 $ai_file = $ai_path . "{$template_id}.ai.json";
716
717 return [
718 'original_file' => $original_file,
719 'ai_file_path' => $ai_file,
720 'ai_directory' => $ai_path,
721 'tmp_directory' => $tmp_dir,
722 ];
723 }
724
725 /**
726 * Get AI tmp directory path for a session
727 *
728 * @param string $session_id The session ID
729 * @return string The tmp directory path
730 */
731 public static function get_ai_tmp_directory($session_id) {
732 $upload_dir = wp_upload_dir();
733 return trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR . $session_id . DIRECTORY_SEPARATOR;
734 }
735
736 /**
737 * Get AI file path for specific content
738 *
739 * @param string $session_id The session ID
740 * @param string $type The content type
741 * @param string $sub_type The content sub-type (optional)
742 * @param string $content_id The content ID
743 * @return string The AI file path
744 */
745 public static function get_ai_file_path($session_id, $type, $sub_type, $content_id) {
746 $tmp_dir = self::get_ai_tmp_directory($session_id);
747 $file_path = $tmp_dir . $type . DIRECTORY_SEPARATOR;
748 if (!empty($sub_type)) {
749 $file_path .= $sub_type . DIRECTORY_SEPARATOR;
750 }
751 return $file_path . "{$content_id}.ai.json";
752 }
753
754 /**
755 * Check if AI file exists and is valid
756 *
757 * @param string $ai_file_path The AI file path
758 * @return bool True if AI file exists and is valid
759 */
760 public static function has_ai_file($ai_file_path) {
761 if (!file_exists($ai_file_path)) {
762 return false;
763 }
764
765 $file_content = file_get_contents($ai_file_path);
766 if (empty($file_content)) {
767 return false;
768 }
769
770 $decoded = json_decode($file_content, true);
771 return json_last_error() === JSON_ERROR_NONE && !empty($decoded);
772 }
773
774 /**
775 * Check if AI file is marked as skipped
776 *
777 * @param string $ai_file_path The AI file path
778 * @return bool True if AI file exists and is marked as skipped
779 */
780 public static function is_ai_file_skipped($ai_file_path) {
781 if (!file_exists($ai_file_path)) {
782 return false;
783 }
784
785 $ai_content = Utils::read_json_file($ai_file_path);
786 return isset($ai_content['isSkipped']) && $ai_content['isSkipped'];
787 }
788
789 /**
790 * Check if content ID is in AI page IDs
791 *
792 * @param string $content_id The content ID to check
793 * @param array $ai_page_ids The AI page IDs structure
794 * @return bool True if content ID is found in AI page IDs
795 */
796 public static function is_ai_content($content_id, $ai_page_ids) {
797 if (empty($ai_page_ids) || !is_array($ai_page_ids)) {
798 return false;
799 }
800
801 foreach ($ai_page_ids as $ids) {
802 if (is_array($ids) && in_array($content_id, $ids)) {
803 return true;
804 }
805 }
806
807 return false;
808 }
809
810 /**
811 * Check if content should be processed as AI content
812 *
813 * @param string $session_id The session ID
814 * @param string $type The content type
815 * @param string $sub_type The content sub-type
816 * @param string $content_id The content ID
817 * @param array $ai_page_ids The AI page IDs structure
818 * @param string $dir_path The base directory path
819 * @return bool True if this is AI content
820 */
821 public static function should_process_as_ai_content($session_id, $type, $sub_type, $content_id, $ai_page_ids, $dir_path) {
822 // Check if content ID is in AI page IDs list
823 $is_ai_template = self::is_ai_content($content_id, $ai_page_ids);
824
825 // Check if AI file exists
826 $paths = self::generate_ai_file_paths($session_id, $type, $sub_type, $content_id, $dir_path);
827 $has_ai_file = self::has_ai_file($paths['ai_file_path']);
828
829 return $is_ai_template || $has_ai_file;
830 }
831
832 /**
833 * Validate and extract AI process data for a given process ID
834 *
835 * @param string $process_id The process ID
836 * @return array|WP_Error Returns process data array or WP_Error on failure
837 */
838 public static function validate_and_get_process_data($process_id) {
839 if (empty($process_id)) {
840 return Helper::error('invalid_process_id', __('Process ID is required.', 'templately'), 'validate_process_data', 400);
841 }
842
843 $ai_process_data = self::get_ai_process_data();
844 if (empty($ai_process_data[$process_id])) {
845 return Helper::error('process_not_found', __('Process ID not found.', 'templately'), 'validate_process_data', 404);
846 }
847
848 $process_data = $ai_process_data[$process_id];
849
850 // Validate required fields
851 if (empty($process_data['session_id'])) {
852 return Helper::error('missing_session_id', __('Session ID missing from process data.', 'templately'), 'validate_process_data', 400);
853 }
854
855 if (empty($process_data['ai_page_ids'])) {
856 return Helper::error('missing_ai_page_ids', __('AI page IDs missing from process data.', 'templately'), 'validate_process_data', 400);
857 }
858
859 return $process_data;
860 }
861
862 /**
863 * Get processed pages data for a process ID
864 *
865 * @param string $process_id The process ID
866 * @return array The processed pages data
867 */
868 public static function get_processed_pages_data($process_id) {
869 $processed_pages = get_option("templately_ai_processed_pages", []);
870 return $processed_pages[$process_id] ?? [];
871 }
872
873 /**
874 * Update processed pages data for a process ID
875 *
876 * @param string $process_id The process ID
877 * @param array $data The data to update
878 * @return bool True on success, false on failure
879 */
880 public static function update_processed_pages_data($process_id, $data) {
881 $processed_pages = get_option("templately_ai_processed_pages", []);
882 $processed_pages[$process_id] = array_merge($processed_pages[$process_id] ?? [], $data);
883 return update_option("templately_ai_processed_pages", $processed_pages, false);
884 }
885
886 /**
887 * Find content type and sub-type for a given content ID in AI page IDs
888 *
889 * @param string $content_id The content ID to find
890 * @param array $ai_page_ids The AI page IDs structure
891 * @return array|null Array with 'type' and 'sub_type' keys, or null if not found
892 */
893 public static function find_content_type_info($content_id, $ai_page_ids) {
894 if (empty($ai_page_ids) || !is_array($ai_page_ids)) {
895 return null;
896 }
897
898 foreach ($ai_page_ids as $key => $ids) {
899 if (is_array($ids) && in_array($content_id, $ids)) {
900 $type_parts = explode('/', $key);
901 return [
902 'type' => $type_parts[0],
903 'sub_type' => isset($type_parts[1]) ? $type_parts[1] : '',
904 'key' => $key
905 ];
906 }
907 }
908
909 return null;
910 }
911
912 /**
913 * Read AI template data directly from files
914 * Common function for reading template data, used by AI content operations
915 *
916 * @param string $session_id The session ID
917 * @param array $ai_page_ids The AI page IDs structure
918 * @param string $dir_path The base directory path
919 * @return array Array of template data indexed by content ID
920 */
921 public static function read_ai_template_data($session_id, $ai_page_ids, $dir_path) {
922 $result = [];
923
924 if (empty($ai_page_ids) || !is_array($ai_page_ids)) {
925 return $result;
926 }
927
928 foreach ($ai_page_ids as $key => $ids) {
929 if (!is_array($ids)) {
930 continue;
931 }
932
933 foreach ($ids as $id) {
934 $type_arr = explode('/', $key);
935 $type = $type_arr[0];
936 $sub_type = isset($type_arr[1]) ? $type_arr[1] : '';
937
938 // Check if this is AI content before processing
939 if (self::should_process_as_ai_content($session_id, $type, $sub_type, $id, $ai_page_ids, $dir_path)) {
940 // Generate AI file paths
941 $ai_paths = self::generate_ai_file_paths($session_id, $type, $sub_type, $id, $dir_path);
942 $ai_file_path = $ai_paths['ai_file_path'];
943
944 if (file_exists($ai_file_path)) {
945 $ai_content = Utils::read_json_file($ai_file_path);
946 if (isset($ai_content['isSkipped']) && $ai_content['isSkipped']) {
947 // Return empty array for skipped content (for JS compatibility)
948 $result[$id] = [];
949 } else {
950 $result[$id] = $ai_content; // Raw AI content
951 }
952 }
953 }
954 }
955 }
956
957 return $result;
958 }
959
960 }
961