"", "releases" => []]; } throw new RuntimeException("Manifest file is missing."); } $decoded = json_decode((string) file_get_contents($manifestFile), true); if (!is_array($decoded)) { throw new RuntimeException("Manifest file is not valid JSON."); } return [ "latest" => trim((string) ($decoded["latest"] ?? "")), "releases" => isset($decoded["releases"]) && is_array($decoded["releases"]) ? $decoded["releases"] : [], ]; } function updateServerWriteManifest(string $manifestFile, array $manifest): void { $json = json_encode( $manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, ); if ($json === false) { throw new RuntimeException("Manifest cannot be encoded."); } $tmpFile = $manifestFile . ".tmp"; if (file_put_contents($tmpFile, $json . PHP_EOL, LOCK_EX) === false) { throw new RuntimeException("Manifest cannot be written."); } @chmod($tmpFile, 0664); if (!rename($tmpFile, $manifestFile)) { @unlink($tmpFile); throw new RuntimeException("Manifest cannot be saved."); } @chmod($manifestFile, 0664); } function updateServerLatestRelease(array $manifest): array { $latest = trim((string) ($manifest["latest"] ?? "")); $releases = isset($manifest["releases"]) && is_array($manifest["releases"]) ? $manifest["releases"] : []; if ($latest === "" || !updateServerVersionIsValid($latest)) { throw new RuntimeException("No valid latest release is configured."); } if (!isset($releases[$latest]) || !is_array($releases[$latest])) { throw new RuntimeException("Latest release entry is missing."); } return updateServerNormalizeRelease($latest, $releases[$latest]); } function updateServerReleaseByVersion(array $manifest, string $version): array { if (!updateServerVersionIsValid($version)) { throw new InvalidArgumentException("Invalid version."); } $releases = isset($manifest["releases"]) && is_array($manifest["releases"]) ? $manifest["releases"] : []; if (!isset($releases[$version]) || !is_array($releases[$version])) { throw new RuntimeException("Release is not configured."); } return updateServerNormalizeRelease($version, $releases[$version]); } function updateServerNormalizeRelease(string $expectedVersion, array $release): array { $version = trim((string) ($release["version"] ?? $expectedVersion)); $package = trim((string) ($release["package"] ?? "")); $sha256 = strtolower(trim((string) ($release["sha256"] ?? ""))); $publishedAt = trim((string) ($release["published_at"] ?? "")); $size = isset($release["size"]) ? (int) $release["size"] : 0; if ($version !== $expectedVersion || !updateServerVersionIsValid($version)) { throw new RuntimeException("Release version is invalid."); } if (!preg_match('/^[a-f0-9]{64}$/', $sha256)) { throw new RuntimeException("Release checksum is invalid."); } if ($package === "" || str_contains($package, "\0") || str_starts_with($package, "/")) { throw new RuntimeException("Release package path is invalid."); } return [ "version" => $version, "package" => $package, "sha256" => $sha256, "size" => $size, "published_at" => $publishedAt, ]; } function updateServerPackagePath(string $baseDir, string $package): string { $packagePath = realpath($baseDir . "/" . $package); $packagesDir = realpath($baseDir . "/packages"); if ( $packagePath === false || $packagesDir === false || !str_starts_with($packagePath, $packagesDir . DIRECTORY_SEPARATOR) || !is_file($packagePath) ) { throw new RuntimeException("Release package is missing."); } return $packagePath; } function updateServerPackageUrl(string $version): string { $scheme = "http"; if ( (!empty($_SERVER["HTTPS"]) && $_SERVER["HTTPS"] !== "off") || (isset($_SERVER["SERVER_PORT"]) && (int) $_SERVER["SERVER_PORT"] === 443) || strtolower((string) ($_SERVER["HTTP_X_FORWARDED_PROTO"] ?? "")) === "https" ) { $scheme = "https"; } $host = $_SERVER["HTTP_HOST"] ?? "localhost"; $scriptDir = rtrim(str_replace("\\", "/", dirname($_SERVER["SCRIPT_NAME"] ?? "")), "/"); return $scheme . "://" . $host . ($scriptDir === "" ? "" : $scriptDir) . "/package.php?version=" . rawurlencode($version); } function updateServerPackageFileName(string $version): string { $prefix = defined("UPDATE_SERVER_PACKAGE_PREFIX") ? (string) UPDATE_SERVER_PACKAGE_PREFIX : "release"; $prefix = trim((string) preg_replace('/[^A-Za-z0-9._-]+/', "-", $prefix), ".-_"); if ($prefix === "") { $prefix = "release"; } return $prefix . "-" . $version . ".zip"; } function updateServerUploadedFileIsZip(array $file): bool { $name = strtolower((string) ($file["name"] ?? "")); $tmpName = (string) ($file["tmp_name"] ?? ""); if (!str_ends_with($name, ".zip") || !is_uploaded_file($tmpName)) { return false; } $handle = fopen($tmpName, "rb"); if ($handle === false) { return false; } $signature = fread($handle, 4); fclose($handle); return $signature === "PK\x03\x04" || $signature === "PK\x05\x06" || $signature === "PK\x07\x08"; } function updateServerPublishUpload( string $version, array $file, string $manifestFile, string $packagesDir, ): void { if (!updateServerVersionIsValid($version)) { throw new RuntimeException("Version must use the format vX.Y.Z."); } if (($file["error"] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { throw new RuntimeException("Upload failed with error code " . (string) ($file["error"] ?? "unknown") . "."); } if (!updateServerUploadedFileIsZip($file)) { throw new RuntimeException("Uploaded file must be a ZIP package."); } updateServerEnsureDirectory($packagesDir); $fileName = updateServerPackageFileName($version); $targetPath = $packagesDir . DIRECTORY_SEPARATOR . $fileName; if (!move_uploaded_file((string) $file["tmp_name"], $targetPath)) { throw new RuntimeException("Uploaded package cannot be stored."); } @chmod($targetPath, 0664); $sha256 = strtolower(hash_file("sha256", $targetPath) ?: ""); $size = filesize($targetPath); if (!preg_match('/^[a-f0-9]{64}$/', $sha256) || $size === false || $size <= 0) { @unlink($targetPath); throw new RuntimeException("Stored package could not be verified."); } $manifest = updateServerReadManifest($manifestFile, true); $manifest["latest"] = $version; $manifest["releases"][$version] = [ "version" => $version, "package" => "packages/" . $fileName, "sha256" => $sha256, "size" => $size, "published_at" => date(DATE_ATOM), ]; ksort($manifest["releases"]); updateServerWriteManifest($manifestFile, $manifest); } function updateServerSetLatest(string $version, string $manifestFile): void { if (!updateServerVersionIsValid($version)) { throw new RuntimeException("Invalid release version."); } $manifest = updateServerReadManifest($manifestFile, true); if (!isset($manifest["releases"][$version])) { throw new RuntimeException("Release is not present in the manifest."); } $manifest["latest"] = $version; updateServerWriteManifest($manifestFile, $manifest); } function updateServerDeleteRelease(string $version, string $manifestFile, string $baseDir): void { if (!updateServerVersionIsValid($version)) { throw new RuntimeException("Invalid release version."); } $manifest = updateServerReadManifest($manifestFile, true); if (!isset($manifest["releases"][$version])) { throw new RuntimeException("Release is not present in the manifest."); } $package = trim((string) ($manifest["releases"][$version]["package"] ?? "")); unset($manifest["releases"][$version]); if ($manifest["latest"] === $version) { $manifest["latest"] = ""; } updateServerWriteManifest($manifestFile, $manifest); if ($package === "" || str_contains($package, "\0") || str_starts_with($package, "/")) { return; } try { $packagePath = updateServerPackagePath($baseDir, $package); unlink($packagePath); } catch (Throwable $exception) { return; } }