self::MAX_ARCHIVE_BYTES) { return new WP_Error('aiwest_zip_too_large', __('The ZIP file is empty or exceeds the 25 MB limit.', 'ai-webmaster-studio')); } $name = sanitize_file_name($file['name'] ?? ''); if (!preg_match('/\.zip$/i', $name)) { return new WP_Error('aiwest_not_zip', __('Only a .zip file is allowed.', 'ai-webmaster-studio')); } if (!class_exists('ZipArchive')) { return new WP_Error('aiwest_zip_missing', __('The PHP ZipArchive extension is not available on this server.', 'ai-webmaster-studio')); } require_once ABSPATH . 'wp-admin/includes/file.php'; $uploaded = wp_handle_upload($file, [ 'test_form' => false, 'mimes' => ['zip' => 'application/zip'], ]); if (!empty($uploaded['error']) || empty($uploaded['file'])) { return new WP_Error('aiwest_upload_failed', sanitize_text_field($uploaded['error'] ?? __('Could not save the ZIP file.', 'ai-webmaster-studio'))); } $target_zip = wp_normalize_path($uploaded['file']); $filetype = wp_check_filetype_and_ext($target_zip, $name, ['zip' => 'application/zip']); if (($filetype['ext'] ?? '') !== 'zip') { wp_delete_file($target_zip); return new WP_Error('aiwest_not_zip', __('The uploaded file is not a valid ZIP archive.', 'ai-webmaster-studio')); } $base = self::upload_dir(); if (!wp_mkdir_p($base)) { wp_delete_file($target_zip); return new WP_Error('aiwest_upload_dir_failed', __('Could not create the AI Webmaster Studio uploads directory.', 'ai-webmaster-studio')); } $stamp = gmdate('Ymd-His') . '-' . wp_generate_password(6, false, false); $extract_dir = wp_normalize_path($base . 'lovable-' . $stamp . '/'); if (!wp_mkdir_p($extract_dir)) { wp_delete_file($target_zip); return new WP_Error('aiwest_extract_dir_failed', __('Could not create the extraction directory.', 'ai-webmaster-studio')); } WP_Filesystem(); global $wp_filesystem; if (!$wp_filesystem) { wp_delete_file($target_zip); return new WP_Error('aiwest_filesystem_failed', __('The WordPress filesystem is not available.', 'ai-webmaster-studio')); } $zip = new ZipArchive(); $open = $zip->open($target_zip); if ($open !== true) { wp_delete_file($target_zip); return new WP_Error('aiwest_zip_open_failed', __('Could not open the ZIP file.', 'ai-webmaster-studio')); } if ($zip->numFiles > 2500) { $zip->close(); wp_delete_file($target_zip); return new WP_Error('aiwest_zip_too_many_files', __('The ZIP contains too many files. Safety limit: 2,500 files.', 'ai-webmaster-studio')); } $total_extracted = 0; for ($i = 0; $i < $zip->numFiles; $i++) { $stat = $zip->statIndex($i); $entry = str_replace('\\', '/', (string) ($stat['name'] ?? '')); if ($entry === '' || str_ends_with($entry, '/')) continue; if ( str_contains($entry, "\0") || str_starts_with($entry, '/') || preg_match('/^[a-zA-Z]:\//', $entry) || preg_match('#(^|/)\.\.(/|$)#', $entry) || str_contains($entry, '__MACOSX') ) { continue; } $ext = strtolower(pathinfo($entry, PATHINFO_EXTENSION)); if ($ext && !in_array($ext, self::allowed_extensions(), true)) { continue; } $entry_size = absint($stat['size'] ?? 0); if ($entry_size > self::MAX_ENTRY_BYTES || ($total_extracted + $entry_size) > self::MAX_EXTRACTED_BYTES) { $zip->close(); wp_delete_file($target_zip); return new WP_Error('aiwest_zip_expansion_limit', __('The ZIP exceeds the safe extracted-data limit.', 'ai-webmaster-studio')); } $contents = $zip->getFromName($entry, self::MAX_ENTRY_BYTES + 1); if ($contents === false || strlen($contents) > self::MAX_ENTRY_BYTES) { $zip->close(); wp_delete_file($target_zip); return new WP_Error('aiwest_zip_entry_failed', __('Could not safely read a file from the ZIP.', 'ai-webmaster-studio')); } $destination = wp_normalize_path($extract_dir . ltrim($entry, '/')); if (!str_starts_with($destination, trailingslashit($extract_dir))) continue; if (!wp_mkdir_p(dirname($destination)) || !$wp_filesystem->put_contents($destination, $contents, FS_CHMOD_FILE)) { $zip->close(); wp_delete_file($target_zip); return new WP_Error('aiwest_zip_extract_failed', __('Could not safely extract the ZIP.', 'ai-webmaster-studio')); } $total_extracted += strlen($contents); } $zip->close(); $analysis = self::analyze_project($extract_dir); update_option('aiwest_lovable_last_zip', $target_zip); update_option('aiwest_lovable_last_extract_dir', $extract_dir); update_option('aiwest_lovable_last_analysis', $analysis); if (class_exists('AIWEST_Memory')) { AIWEST_Memory::set('lovable_project_summary', self::summary_text($analysis), 'lovable'); } if (class_exists('AIWEST_Logger')) { AIWEST_Logger::info('Design reference ZIP imported.', [ 'zip' => basename($target_zip), 'files' => $analysis['counts']['files'] ?? 0 ]); } return $analysis; } public static function analyze_project($dir) { $allowed = self::allowed_extensions(); $files = []; $components = []; $pages = []; $routes = []; $data_files = []; $assets_refs = []; $snippets = []; $total_bytes_read = 0; $max_files_to_read = 160; $max_bytes_total = 220000; $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS)); foreach ($iterator as $file) { if (!$file->isFile()) continue; $path = $file->getPathname(); $rel = ltrim(str_replace($dir, '', $path), '/\\'); $rel_norm = str_replace('\\', '/', $rel); $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); if (!in_array($ext, $allowed, true)) continue; $files[] = $rel_norm; if (preg_match('#(^|/)components/#i', $rel_norm)) $components[] = $rel_norm; if (preg_match('#(^|/)(pages|views|routes|app)/#i', $rel_norm)) $pages[] = $rel_norm; if (preg_match('#(^|/)(data|content|constants)/#i', $rel_norm)) $data_files[] = $rel_norm; if (count($snippets) < $max_files_to_read && $total_bytes_read < $max_bytes_total) { $size = min(filesize($path), 5000); $raw = file_get_contents($path, false, null, 0, $size); if ($raw !== false) { $total_bytes_read += strlen($raw); $text = self::clean_text($raw); $found = self::extract_interesting($text, $rel_norm); if ($found) $snippets[$rel_norm] = $found; if (preg_match_all('/(?:to|href|path)\s*=\s*["\']([^"\']+)["\']/i', $text, $m)) { foreach ($m[1] as $r) { if (strlen($r) <= 120) $routes[] = $r; } } if (preg_match_all('/(?:src|url)\(([^)]+)\)|src\s*=\s*["\']([^"\']+)["\']/i', $text, $m2)) { foreach ($m2[0] as $ref) { if (strlen($ref) <= 160) $assets_refs[] = $ref; } } } } } $routes = array_values(array_unique(array_slice($routes, 0, 80))); $components = array_values(array_unique(array_slice($components, 0, 120))); $pages = array_values(array_unique(array_slice($pages, 0, 80))); $data_files = array_values(array_unique(array_slice($data_files, 0, 80))); $analysis = [ 'created_at' => current_time('mysql'), 'project_dir' => $dir, 'counts' => [ 'files' => count($files), 'components' => count($components), 'pages' => count($pages), 'data_files' => count($data_files), 'routes' => count($routes), ], 'components' => $components, 'pages' => $pages, 'data_files' => $data_files, 'routes' => $routes, 'snippets' => $snippets, 'summary' => '', ]; $analysis['summary'] = self::summary_text($analysis); return $analysis; } public static function clean_text($text) { $text = preg_replace('/\s+/', ' ', $text); $text = wp_strip_all_tags($text); return trim($text); } public static function extract_interesting($text, $rel) { $out = []; if (preg_match_all('/(?:title|label|name|heading|text)\s*[:=]\s*[`"\']([^`"\']{3,120})[`"\']/i', $text, $m)) { $out['labels'] = array_values(array_unique(array_slice($m[1], 0, 12))); } if (preg_match_all('/]*>(.*?)<\/h[1-3]>/i', $text, $m2)) { $heads = array_map('wp_strip_all_tags', $m2[1]); $out['headings'] = array_values(array_unique(array_slice($heads, 0, 12))); } if (preg_match_all('/className\s*=\s*["\']([^"\']{3,180})["\']/i', $text, $m3)) { $out['classes'] = array_values(array_unique(array_slice($m3[1], 0, 8))); } if (!$out) { $short = wp_trim_words($text, 40, '...'); if (strlen($short) > 40) $out['excerpt'] = $short; } return $out; } public static function summary_text($analysis) { $lines = []; $lines[] = 'Imported project design reference.'; $lines[] = 'Import date: ' . ($analysis['created_at'] ?? ''); $lines[] = 'Files: ' . ($analysis['counts']['files'] ?? 0); $lines[] = 'Components: ' . ($analysis['counts']['components'] ?? 0); $lines[] = 'Pages/views: ' . ($analysis['counts']['pages'] ?? 0); $lines[] = 'Data files: ' . ($analysis['counts']['data_files'] ?? 0); if (!empty($analysis['routes'])) { $lines[] = 'Detected routes/menu/links: ' . implode(', ', array_slice($analysis['routes'], 0, 25)); } if (!empty($analysis['pages'])) { $lines[] = 'Important page files: ' . implode(', ', array_slice($analysis['pages'], 0, 20)); } if (!empty($analysis['components'])) { $lines[] = 'Important components: ' . implode(', ', array_slice($analysis['components'], 0, 30)); } if (!empty($analysis['data_files'])) { $lines[] = 'Data/content files: ' . implode(', ', array_slice($analysis['data_files'], 0, 20)); } if (!empty($analysis['snippets'])) { $lines[] = 'Content and class excerpts:'; $i = 0; foreach ($analysis['snippets'] as $file => $data) { $lines[] = '- ' . $file . ': ' . wp_json_encode($data, JSON_UNESCAPED_UNICODE); $i++; if ($i >= 30) break; } } return implode("\n", $lines); } public static function last_analysis() { $analysis = get_option('aiwest_lovable_last_analysis', []); return is_array($analysis) ? $analysis : []; } }