isDir()) { rmdir($item->getPathname()); } else { unlink($item->getPathname()); } } rmdir($dir); } function updaterHttpGet(string $url, int $timeout = 30): string { if (!filter_var($url, FILTER_VALIDATE_URL)) { throw new RuntimeException("Invalid URL: " . $url); } $context = stream_context_create([ "http" => [ "method" => "GET", "timeout" => $timeout, "ignore_errors" => true, "header" => "User-Agent: PSA-Orderform-Updater/" . APP_VERSION . "\r\n", ], ]); $body = @file_get_contents($url, false, $context); $status = 0; $responseHeaders = function_exists("http_get_last_response_headers") ? http_get_last_response_headers() : []; if (is_array($responseHeaders)) { foreach ($responseHeaders as $header) { if (preg_match('/^HTTP\/\S+\s+(\d+)/', $header, $matches)) { $status = (int) $matches[1]; } } } if ($body === false || ($status >= 400 && $status < 600)) { throw new RuntimeException( "HTTP request failed" . ($status > 0 ? " with status " . $status : "") . ".", ); } return $body; } function updaterFetchManifest(): array { $url = trim((string) UPDATE_MANIFEST_URL); if ($url === "") { throw new RuntimeException("UPDATE_MANIFEST_URL is not configured."); } $body = updaterHttpGet($url, 15); $manifest = json_decode($body, true); if (!is_array($manifest)) { throw new RuntimeException("Manifest response is not valid JSON."); } $version = trim((string) ($manifest["version"] ?? $manifest["latest"] ?? "")); $packageUrl = trim((string) ($manifest["package_url"] ?? "")); $sha256 = strtolower(trim((string) ($manifest["sha256"] ?? ""))); $size = isset($manifest["size"]) ? (int) $manifest["size"] : 0; $publishedAt = trim((string) ($manifest["published_at"] ?? "")); if (!updaterIsVersion($version)) { throw new RuntimeException("Manifest version is invalid."); } if (!filter_var($packageUrl, FILTER_VALIDATE_URL)) { throw new RuntimeException("Manifest package URL is invalid."); } if (!preg_match('/^[a-f0-9]{64}$/', $sha256)) { throw new RuntimeException("Manifest checksum is invalid."); } return [ "version" => $version, "package_url" => $packageUrl, "sha256" => $sha256, "size" => $size, "published_at" => $publishedAt, ]; } function updaterDownloadPackage(array $manifest, string $targetFile): void { updaterEnsureDirectory(dirname($targetFile)); $data = updaterHttpGet($manifest["package_url"], 120); if ($data === "") { throw new RuntimeException("Downloaded package is empty."); } if (file_put_contents($targetFile, $data, LOCK_EX) === false) { throw new RuntimeException("Downloaded package cannot be written."); } if ($manifest["size"] > 0 && filesize($targetFile) !== $manifest["size"]) { unlink($targetFile); throw new RuntimeException("Downloaded package size mismatch."); } $actualHash = strtolower(hash_file("sha256", $targetFile) ?: ""); if ($actualHash !== $manifest["sha256"]) { unlink($targetFile); throw new RuntimeException("Package checksum mismatch."); } } function updaterValidateZipEntry(string $entry): bool { $entry = str_replace("\\", "/", $entry); $normalized = trim($entry, "/"); if ( $normalized === "" || str_contains($entry, "\0") || str_starts_with($entry, "/") || preg_match('/^[A-Za-z]:\//', $entry) ) { return false; } foreach (explode("/", $normalized) as $segment) { if ($segment === "" || $segment === "." || $segment === "..") { return false; } } return true; } function updaterExtractPackage(string $zipFile, string $stageDir): void { if (!class_exists("ZipArchive")) { throw new RuntimeException("PHP ZipArchive extension is not available."); } updaterRemoveDirectory($stageDir); updaterEnsureDirectory($stageDir); $zip = new ZipArchive(); if ($zip->open($zipFile) !== true) { throw new RuntimeException("Downloaded package is not a readable ZIP file."); } $hasAppFile = false; for ($i = 0; $i < $zip->numFiles; $i++) { $name = (string) $zip->getNameIndex($i); if (!updaterValidateZipEntry($name)) { $zip->close(); throw new RuntimeException("ZIP contains an unsafe path: " . $name); } if ( $name === "index.php" || str_starts_with($name, "admin/") || str_starts_with($name, "includes/") ) { $hasAppFile = true; } } if (!$hasAppFile) { $zip->close(); throw new RuntimeException("ZIP does not look like an app-root release package."); } if (!$zip->extractTo($stageDir)) { $zip->close(); throw new RuntimeException("ZIP package cannot be extracted."); } $zip->close(); } function updaterRelativePath(string $path, string $baseDir): string { return ltrim(str_replace("\\", "/", substr($path, strlen($baseDir))), "/"); } function updaterShouldSkipPath(string $relativePath): bool { $relativePath = trim(str_replace("\\", "/", $relativePath), "/"); return $relativePath === "" || $relativePath === "config.php" || $relativePath === "data" || str_starts_with($relativePath, "data/") || $relativePath === ".git" || str_starts_with($relativePath, ".git/"); } function updaterCopyWithBackup(string $stageDir, string $appRoot, string $backupDir): array { updaterEnsureDirectory($backupDir); $copied = 0; $backedUp = 0; $skipped = 0; $items = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($stageDir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST, ); foreach ($items as $item) { $relativePath = updaterRelativePath($item->getPathname(), $stageDir); if (updaterShouldSkipPath($relativePath)) { $skipped++; continue; } $targetPath = $appRoot . DIRECTORY_SEPARATOR . $relativePath; if ($item->isDir()) { updaterEnsureDirectory($targetPath); continue; } updaterEnsureDirectory(dirname($targetPath)); if (file_exists($targetPath)) { $backupPath = $backupDir . DIRECTORY_SEPARATOR . $relativePath; updaterEnsureDirectory(dirname($backupPath)); if (!copy($targetPath, $backupPath)) { throw new RuntimeException("Cannot back up file: " . $relativePath); } $backedUp++; } if (!copy($item->getPathname(), $targetPath)) { throw new RuntimeException("Cannot deploy file: " . $relativePath); } @chmod($targetPath, fileperms($item->getPathname()) & 0777); $copied++; } return [ "copied" => $copied, "backed_up" => $backedUp, "skipped" => $skipped, ]; } function updaterDeploy(array $manifest, string $appRoot): array { $runId = date("Ymd-His"); $workDir = rtrim((string) UPDATE_WORK_DIR, "/\\") . DIRECTORY_SEPARATOR . $runId; $stageDir = $workDir . DIRECTORY_SEPARATOR . "stage"; $zipFile = $workDir . DIRECTORY_SEPARATOR . "package.zip"; $backupDir = rtrim((string) UPDATE_BACKUP_DIR, "/\\") . DIRECTORY_SEPARATOR . $runId . "-" . $manifest["version"]; updaterEnsureDirectory($workDir); updaterDownloadPackage($manifest, $zipFile); updaterExtractPackage($zipFile, $stageDir); $result = updaterCopyWithBackup($stageDir, $appRoot, $backupDir); updaterRemoveDirectory($workDir); return [ "backup_dir" => $backupDir, "copied" => $result["copied"], "backed_up" => $result["backed_up"], "skipped" => $result["skipped"], ]; } $manifest = null; $updateAvailable = false; try { $manifest = updaterFetchManifest(); $updateAvailable = version_compare( updaterVersionToCompare($manifest["version"]), updaterVersionToCompare(APP_VERSION), ">", ); } catch (Throwable $exception) { $errors[] = $exception->getMessage(); } if ($_SERVER["REQUEST_METHOD"] === "POST") { if (!updaterValidateCsrfToken((string) ($_POST["csrf_token"] ?? ""))) { $errors[] = "Invalid token. Please reload the page and try again."; } elseif ($appRoot === false) { $errors[] = "Application root cannot be resolved."; } else { try { $manifest = updaterFetchManifest(); $force = !empty($_POST["force_redeploy"]); $updateAvailable = version_compare( updaterVersionToCompare($manifest["version"]), updaterVersionToCompare(APP_VERSION), ">", ); if (!$updateAvailable && !$force) { throw new RuntimeException( "No newer update is available. Enable force redeployment to deploy this package anyway.", ); } $result = updaterDeploy($manifest, $appRoot); $messages[] = "Deployment finished."; $messages[] = "Files copied: " . $result["copied"]; $messages[] = "Files backed up: " . $result["backed_up"]; $messages[] = "Skipped preserved paths: " . $result["skipped"]; $messages[] = "Backup directory: " . $result["backup_dir"]; } catch (Throwable $exception) { $errors[] = $exception->getMessage(); } } } ?> Updater

Updater

Back to settings

Error:

Installed version
Update target URL
Available version
Package URL
SHA-256
Published at
Update available

Manual deployment