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/");