open($zipPath) !== true) { die("Error: Unable to open ZIP file."); return false; } // Check for manifest.json at root of ZIP $manifestIndex = $zip->locateName('manifest.json', ZipArchive::FL_NODIR); if ($manifestIndex === false) { die("Error: manifest.json not found in ZIP root."); $zip->close(); return false; } // Read and decode manifest.json $manifestContent = $zip->getFromIndex($manifestIndex); $manifest = json_decode($manifestContent, true); if (!$manifest) { die("Error: manifest.json contains invalid JSON."); $zip->close(); return false; } // Validate manifest fields $errors = validateManifest($manifest); if (!empty($errors)) { echo "Manifest validation errors:\n"; foreach ($errors as $error) { echo " - $error\n"; } $zip->close(); return false; } // App name used for directory (safe slug) $appName = preg_replace('/[^a-z0-9_-]/i', '_', $manifest['name']); $installPath = $appsDir . $appName; // Create app install directory if not exists if (!is_dir($installPath)) { if (!mkdir($installPath, 0755, true)) { echo "Error: Unable to create app directory: $installPath\n"; $zip->close(); return false; } } else { echo ""; } // Extract all ZIP files to the install directory for ($i = 0; $i < $zip->numFiles; $i++) { $stat = $zip->statIndex($i); $filename = $stat['name']; // Security check: prevent path traversal if (strpos($filename, '..') !== false) { echo "Error: ZIP contains invalid filename (path traversal): $filename\n"; $zip->close(); return false; } // Extract file content $content = $zip->getFromIndex($i); // Compute target path $targetPath = $installPath . '/' . $filename; // Create directory if needed $dir = dirname($targetPath); if (!is_dir($dir)) { mkdir($dir); } // Write file file_put_contents($targetPath, $content); } $zip->close(); echo ""; return true; } require_once "vfs.php"; $file = resolve_path($_GET['q'] ?? ''); installPackage($file, __DIR__ . "/apps/");