"data", "glob" => "data/*.json"]]); } if (!defined("MANAGE_BACKUP_DATABASE")) { define("MANAGE_BACKUP_DATABASE", null); } if (!defined("MANAGE_BACKUP_LOCAL_RETENTION")) { define("MANAGE_BACKUP_LOCAL_RETENTION", 4); } if (!defined("MANAGE_BACKUP_AUTO_INTERVAL_SECONDS")) { define("MANAGE_BACKUP_AUTO_INTERVAL_SECONDS", 604800); } if (!defined("MANAGE_BACKUP_COMPRESS")) { define("MANAGE_BACKUP_COMPRESS", true); } if (!defined("MANAGE_BACKUP_UPLOAD")) { define("MANAGE_BACKUP_UPLOAD", true); } if (!defined("MANAGE_BACKUP_REMOTE_TARGETS")) { define("MANAGE_BACKUP_REMOTE_TARGETS", []); } // Raised when a remote upload fails. Carries a scrubbed debug context that is // safe to log: credentials are never part of it. class ManageRemoteUploadException extends RuntimeException { private array $debugContext; public function __construct(string $message, array $debugContext = []) { parent::__construct($message); $this->debugContext = $debugContext; } public function getDebugContext(): array { return $this->debugContext; } } // --------------------------------------------------------------------------- // Paths // --------------------------------------------------------------------------- function manageClientAppRoot(): string { $root = realpath((string) MANAGE_APP_ROOT); if ($root === false) { throw new RuntimeException("MANAGE_APP_ROOT existiert nicht: " . MANAGE_APP_ROOT); } return rtrim($root, "/\\"); } function manageClientPath(string $relative): string { return manageClientAppRoot() . DIRECTORY_SEPARATOR . ltrim($relative, "/\\"); } function manageClientNormalizePath(string $path): string { return str_replace("\\", "/", $path); } function manageEnsureDir(string $dir): void { if (!is_dir($dir) && !mkdir($dir, 02775, true) && !is_dir($dir)) { throw new RuntimeException("Verzeichnis konnte nicht erstellt werden: " . $dir); } @chmod($dir, 02775); } function manageRemoveDir(string $dir): void { if (!is_dir($dir)) { return; } $items = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST, ); foreach ($items as $item) { if ($item->isDir()) { @rmdir($item->getPathname()); } else { @unlink($item->getPathname()); } } @rmdir($dir); } function manageIsTemporaryFile(string $path): bool { $name = basename($path); return $name === "" || $name[0] === "." || str_ends_with($name, ".tmp") || str_ends_with($name, ".part"); } function manageFormatBytes(int $bytes): string { if ($bytes >= 1073741824) { return number_format($bytes / 1073741824, 2, ",", ".") . " GB"; } if ($bytes >= 1048576) { return number_format($bytes / 1048576, 2, ",", ".") . " MB"; } if ($bytes >= 1024) { return number_format($bytes / 1024, 1, ",", ".") . " KB"; } return $bytes . " B"; } // --------------------------------------------------------------------------- // JSON state files // --------------------------------------------------------------------------- function manageReadJson(string $file): array { if (!is_file($file)) { return []; } $content = file_get_contents($file); if ($content === false || trim($content) === "") { return []; } $decoded = json_decode($content, true); return is_array($decoded) ? $decoded : []; } function manageWriteJson(string $file, array $data): void { manageEnsureDir(dirname($file)); $json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($json === false) { throw new RuntimeException("JSON konnte nicht kodiert werden: " . basename($file)); } $tmpFile = $file . ".tmp"; if (file_put_contents($tmpFile, $json . PHP_EOL, LOCK_EX) === false) { throw new RuntimeException("Datei konnte nicht geschrieben werden: " . basename($file)); } @chmod($tmpFile, 0664); if (!rename($tmpFile, $file)) { @unlink($tmpFile); throw new RuntimeException("Datei konnte nicht gespeichert werden: " . basename($file)); } @chmod($file, 0664); } // --------------------------------------------------------------------------- // Logging // --------------------------------------------------------------------------- // Appends one JSON line. Never throws: a failed log write must not abort an // update or a backup. function manageClientLog(string $level, string $message, array $context = []): void { $file = (string) MANAGE_LOG_FILE; if ($file === "") { return; } try { manageEnsureDir(dirname($file)); } catch (Throwable $exception) { return; } // Simple size cap; the host application owns its own log rotation. if (is_file($file) && (int) (filesize($file) ?: 0) > 2097152) { @rename($file, $file . ".1"); } $line = json_encode([ "timestamp" => date("Y-m-d H:i:s"), "level" => $level, "message" => $message, "context" => $context, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if (is_string($line)) { @file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX); } } // --------------------------------------------------------------------------- // Version file // --------------------------------------------------------------------------- function manageIsVersionString(string $version): bool { return preg_match('/^v\d+\.\d+\.\d+$/', trim($version)) === 1; } function manageVersionCompareValue(string $version): string { return ltrim(trim($version), "vV"); } /** * Reads the installed version. Supports both supported layouts: * - a PHP file defining a constant (MANAGE_VERSION_CONSTANT) * - a plain text file containing only the version * * Returns "" when the version cannot be determined, which the callers treat as * "unknown" rather than as an error. */ function manageClientVersion(): string { $file = (string) MANAGE_VERSION_FILE; if ($file === "" || !is_file($file)) { return ""; } $constant = MANAGE_VERSION_CONSTANT; if (is_string($constant) && $constant !== "") { // The constant may already be defined by the host application. if (defined($constant)) { $value = (string) constant($constant); if (manageIsVersionString($value)) { return trim($value); } } // Otherwise parse it out of the file without executing it: the file may // have side effects, and including it twice would fatal on redefinition. $content = (string) file_get_contents($file); $pattern = '/define\s*\(\s*["\']' . preg_quote($constant, "/") . '["\']\s*,\s*["\'](v?\d+\.\d+\.\d+)["\']/'; if (preg_match($pattern, $content, $matches) === 1) { return trim($matches[1]); } return ""; } $value = trim((string) file_get_contents($file)); return manageIsVersionString($value) ? $value : ""; } // --------------------------------------------------------------------------- // HTTP transport // --------------------------------------------------------------------------- function manageClientConfigured(): bool { return trim((string) MANAGE_SERVER_URL) !== "" && trim((string) MANAGE_INSTANCE) !== "" && trim((string) MANAGE_TOKEN) !== ""; } function manageClientRequireConfigured(): void { if (manageClientConfigured()) { return; } throw new RuntimeException( "Manage-Client ist nicht konfiguriert. MANAGE_SERVER_URL, MANAGE_INSTANCE und MANAGE_TOKEN " . "müssen in manage-client/config.php gesetzt sein.", ); } function manageClientEndpoint(string $path): string { $base = rtrim(trim((string) MANAGE_SERVER_URL), "/"); return $base . "/api/v1/" . ltrim($path, "/"); } function manageClientUserAgent(): string { $version = manageClientVersion(); return "Manage-Client/1.0 (" . (string) MANAGE_INSTANCE . "; app " . ($version !== "" ? $version : "unknown") . ")"; } function manageClientAuthHeaders(): string { return "X-Manage-Instance: " . (string) MANAGE_INSTANCE . "\r\n" . "X-Manage-Token: " . (string) MANAGE_TOKEN . "\r\n" . "User-Agent: " . manageClientUserAgent() . "\r\n"; } function manageClientStatusFromHeaders(array $headers): int { $status = 0; foreach ($headers as $header) { if (preg_match('/^HTTP\/\S+\s+(\d+)/', (string) $header, $matches) === 1) { $status = (int) $matches[1]; } } return $status; } function manageClientResponseHeaders($legacyHeaders): array { if (function_exists("http_get_last_response_headers")) { $lastHeaders = http_get_last_response_headers(); return is_array($lastHeaders) ? $lastHeaders : []; } return is_array($legacyHeaders) ? $legacyHeaders : []; } // Turns a server error response into a message worth reading. The API always // answers with {"success":false,"error":"..."}; anything else is truncated. function manageClientErrorMessage(int $status, $body): string { $suffix = $status > 0 ? " (HTTP " . $status . ")" : ""; if (is_string($body) && $body !== "") { $decoded = json_decode($body, true); if (is_array($decoded) && isset($decoded["error"])) { return trim((string) $decoded["error"]) . $suffix; } $excerpt = substr(trim(preg_replace('/\s+/', " ", $body) ?? ""), 0, 300); if ($excerpt !== "") { return $excerpt . $suffix; } } return "Anfrage fehlgeschlagen" . ($suffix !== "" ? $suffix : " (keine Antwort vom Server)") . "."; } /** * Performs an authenticated request against the manage server. * * @param string $method GET or POST * @param string $path endpoint below api/v1/ * @param string|null $body raw request body for POST * @param string $contentType * @param int|null $timeout seconds; defaults to MANAGE_HTTP_TIMEOUT * * @return array{status: int, body: string} */ function manageClientRequest( string $method, string $path, ?string $body = null, string $contentType = "application/json", ?int $timeout = null, ): array { manageClientRequireConfigured(); $url = manageClientEndpoint($path); if (!filter_var($url, FILTER_VALIDATE_URL)) { throw new RuntimeException("Ungültige Server-URL: " . $url); } $headers = manageClientAuthHeaders() . "Accept: application/json\r\n"; $options = [ "method" => $method, "timeout" => $timeout ?? (int) MANAGE_HTTP_TIMEOUT, "ignore_errors" => true, "follow_location" => 0, "protocol_version" => 1.1, ]; if ($body !== null) { $headers .= "Content-Type: " . $contentType . "\r\n"; $headers .= "Content-Length: " . strlen($body) . "\r\n"; $options["content"] = $body; } $options["header"] = $headers; $context = stream_context_create(["http" => $options]); $response = @file_get_contents($url, false, $context); $status = manageClientStatusFromHeaders(manageClientResponseHeaders($http_response_header ?? null)); if ($response === false && $status === 0) { throw new RuntimeException("Manage-Server ist nicht erreichbar: " . $url); } return ["status" => $status, "body" => is_string($response) ? $response : ""]; } /** * Authenticated request that expects a JSON object and a 2xx status. */ function manageClientRequestJson( string $method, string $path, ?array $payload = null, ?int $timeout = null, ): array { $body = $payload === null ? null : json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); if ($payload !== null && $body === false) { throw new RuntimeException("Anfrage konnte nicht kodiert werden."); } $response = manageClientRequest($method, $path, $body, "application/json", $timeout); if ($response["status"] < 200 || $response["status"] >= 300) { throw new RuntimeException(manageClientErrorMessage($response["status"], $response["body"])); } $decoded = json_decode($response["body"], true); if (!is_array($decoded)) { throw new RuntimeException("Antwort des Servers ist kein gültiges JSON."); } return $decoded; } require_once __DIR__ . "/zip.php"; require_once __DIR__ . "/mysql.php"; require_once __DIR__ . "/remote.php"; require_once __DIR__ . "/backup.php"; require_once __DIR__ . "/hooks.php"; require_once __DIR__ . "/updater.php"; require_once __DIR__ . "/heartbeat.php"; /** * Aggregate status used by the CLI, the GUI panel and any host integration. * Never throws: every remote failure is reported inside the returned array, so * a settings page can render even when the server is unreachable. */ function manageClientStatus(): array { $status = [ "instance" => (string) MANAGE_INSTANCE, "server_url" => (string) MANAGE_SERVER_URL, "configured" => manageClientConfigured(), "version" => manageClientVersion(), "php_version" => PHP_VERSION, "update" => null, "update_error" => null, "backups" => [], "last_backup_at" => null, "pending_migrations" => [], "errors" => [], ]; try { $status["backups"] = manageBackupList(); $status["last_backup_at"] = $status["backups"] === [] ? null : (string) ($status["backups"][0]["created_at"] ?? ""); } catch (Throwable $exception) { $status["errors"][] = $exception->getMessage(); } try { $status["pending_migrations"] = manageUpdatePendingMigrations(); } catch (Throwable $exception) { $status["errors"][] = $exception->getMessage(); } if ($status["configured"]) { try { $status["update"] = manageUpdateCheck(); } catch (Throwable $exception) { $status["update_error"] = $exception->getMessage(); } } return $status; }