| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310 |
- <?php
- declare(strict_types=1);
- function updateServerJsonResponse(int $status, array $payload): void
- {
- http_response_code($status);
- header("Content-Type: application/json; charset=utf-8");
- echo json_encode(
- $payload,
- JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE,
- );
- exit;
- }
- function updateServerTextResponse(int $status, string $message): void
- {
- http_response_code($status);
- header("Content-Type: text/plain; charset=utf-8");
- echo $message;
- exit;
- }
- function updateServerVersionIsValid(string $version): bool
- {
- return preg_match('/^v\d+\.\d+\.\d+$/', $version) === 1;
- }
- function updateServerEnsureDirectory(string $dir): void
- {
- if (!is_dir($dir) && !mkdir($dir, 02775, true) && !is_dir($dir)) {
- throw new RuntimeException("Directory cannot be created: " . $dir);
- }
- @chmod($dir, 02775);
- }
- function updateServerReadManifest(string $manifestFile, bool $allowMissing = false): array
- {
- if (!is_file($manifestFile)) {
- if ($allowMissing) {
- return ["latest" => "", "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;
- }
- }
|